Skip to main content

zsh/ported/zle/
compcore.rs

1//! Direct port of `Src/Zle/compcore.c` — completion core code.
2//!
3//! Original C copyright: Sven Wischnowsky 1995-1997.
4//!
5//! C source is 3,638 lines. This file ports:
6//!   - the file-scope globals (c:36-279)
7//!   - the pure-string helpers (`rembslash`, `remsquote`,
8//!     `comp_quoting_string`, `multiquote`, `tildequote`, `matcheq`,
9//!     `matchcmp`, `ctokenize`, `comp_str`)
10//!   - the linked-list group manipulators (`begcmgroup`,
11//!     `endcmgroup`, `addexpl`, `addmatch`)
12//!   - the param-table helpers (`get_user_var`, `get_data_arr`,
13//!     `set_list_array`)
14//!   - the hook entry points (`before_complete`, `after_complete`)
15//!     in their non-runhookdef branches
16//!
17//! Functions blocked on heavier substrate (`do_completion`,
18//! `makecomplist`, `addmatches`, `callcompfunc`, `set_comp_sep`,
19//! `check_param`, `permmatches`, `dupmatch`, `add_match_data`,
20//! `makearray`) carry doc comments naming the missing dependencies.
21
22#![allow(non_snake_case, non_upper_case_globals, dead_code)]
23
24use crate::ported::context::zcontext_restore_partial;
25use crate::ported::module::{gethookdef, runhookdef};
26use crate::ported::params::{getsparam, paramtab, paramtab_hashed_storage, setaparam, setsparam};
27use crate::ported::signals::{queue_signals, unqueue_signals};
28use crate::ported::zle::comp_h::{
29    Aminfo, Brinfo, Cadata, Ccmakedat, Cexpl, Cline, Cmatch, Cmgroup, Cmlist, Menuinfo, CAF_ALL,
30    CAF_MATCH, CAF_MATSORT, CAF_NOSORT, CAF_NUMSORT, CAF_QUOTE, CAF_REVSORT, CAF_UNIQALL,
31    CAF_UNIQCON, CGF_MATSORT, CGF_NOSORT, CGF_NUMSORT, CGF_REVSORT, CGF_UNIQALL, CGF_UNIQCON,
32    CMF_DELETE, CMF_DISPLINE, CMF_FMULT, CMF_MULT, CMF_NOLIST, CMF_PACKED, CMF_PARBR, CMF_PARNEST,
33    CMF_ROWS,
34};
35use crate::ported::zle::complete::{
36    COMPIPREFIX, COMPLIST, COMPPREFIX, COMPQSTACK, COMPSUFFIX, INCOMPFUNC,
37};
38use crate::ported::zle::compmatch::{bld_parts, cline_matched};
39use crate::ported::zle::compresult::{do_ambig_menu, ztat};
40use crate::ported::zle::zle_h::{invalidatelist, COMP_LIST_COMPLETE, COMP_LIST_EXPAND, CUT_RAW};
41use crate::ported::zle::zle_refresh::{CLEARLIST, SHOWINGLIST};
42use crate::ported::zle::zle_tricky::{
43    inststr, MENUCMP, ORIGCS, ORIGLINE, USEGLOB, USEMENU, VALIDLIST, WOULDINSTAB,
44};
45use crate::ported::zle::zle_utils::foredel;
46#[allow(unused_imports)]
47use crate::ported::zle::{
48    deltochar::*, textobjects::*, zle_h::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
49    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
50};
51use crate::ported::zsh_h::{
52    isset, Bnull, Dnull, Equals, Hat, Inbrace, Inbrack, Inpar, Outbrace, Outpar, Pound, Qstring,
53    Quest, Snull, Star, Stringg, Tilde, BASHAUTOLIST, NUMERICGLOBSORT, PM_HASHED, PM_TYPE,
54    QT_BACKSLASH, QT_DOLLARS, QT_DOUBLE, QT_NONE, QT_SINGLE, RCQUOTES, SORTIT_IGNORING_BACKSLASHES,
55    SORTIT_NUMERICALLY, ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE,
56};
57use crate::DPUTS;
58use std::sync::atomic::{AtomicI32, Ordering};
59use std::sync::{Mutex, OnceLock};
60
61// =====================================================================
62// Substrate-blocked stubs — bodies need substrate listed in each
63// doc comment. Returns shape-correct safe defaults.
64// =====================================================================
65
66// =====================================================================
67// do_completion — `Src/Zle/compcore.c:287`.
68// =====================================================================
69
70/// Direct port of `int do_completion(Hookdef dummy, Compldat dat)`
71/// from compcore.c:287. The top-level completion driver: per-round
72/// state reset → `makecomplist` → dispatch to `do_ambiguous` /
73/// `do_single` / `do_allmatches` per result count.
74pub fn do_completion(s: &str, incmd: i32, lst: i32) -> i32 {
75    // c:287
76
77    let osl = SHOWINGLIST.load(Ordering::Relaxed); // c:289
78    let mut ret: i32 = 0; // c:289
79
80    // c:296-297 — `ainfo = fainfo = NULL`.
81    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
82        *g = None;
83    }
84    if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
85        *g = None;
86    }
87    if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
88        g.clear(); // c:298
89    }
90
91    // c:300-307 — compqstack reset.
92    let instring = INSTRING.load(Ordering::Relaxed); // c:307
93                                                     // c:305 — `compqstack = instring == QT_NONE ? "\\" : <quote-char>`.
94                                                     // Inlined `char_from_qt(x)` as `(x as u8) as char`.
95    let head_q: char = if instring == QT_NONE {
96        // c:305
97        QT_BACKSLASH as u8 as char
98    } else {
99        instring as u8 as char
100    };
101    if let Ok(mut g) = compqstack.get_or_init(|| Mutex::new(String::new())).lock() {
102        *g = head_q.to_string(); // c:305-306
103    }
104
105    hasunqu.store(0, Ordering::Relaxed); // c:309
106    let wouldinstab_v = WOULDINSTAB.load(Ordering::Relaxed); // c:310
107    useline.store(
108        // c:310
109        if wouldinstab_v != 0 {
110            -1
111        } else if lst != COMP_LIST_COMPLETE {
112            1
113        } else {
114            0
115        },
116        Ordering::Relaxed,
117    );
118    useexact.store(opt_isset("RECEXACT"), Ordering::Relaxed); // c:311
119    set_compstate_str("exact_string", ""); // c:312
120    let useline_v = useline.load(Ordering::Relaxed);
121    uselist.store(
122        // c:314
123        if useline_v != 0 {
124            if opt_isset("AUTOLIST") != 0 && opt_isset("BASHAUTOLIST") == 0 {
125                if opt_isset("LISTAMBIGUOUS") != 0 {
126                    3
127                } else {
128                    2
129                }
130            } else {
131                0
132            }
133        } else {
134            1
135        },
136        Ordering::Relaxed,
137    );
138
139    let useglob_v = USEGLOB.load(Ordering::Relaxed); // c:319
140    let opm: String = if useglob_v != 0 {
141        "*".into()
142    } else {
143        "".into()
144    };
145    if let Ok(mut g) = comppatmatch.get_or_init(|| Mutex::new(None)).lock() {
146        *g = Some(opm.clone()); // c:319
147    }
148    set_compstate_str("pattern_insert", "menu"); // c:320
149    forcelist.store(0, Ordering::Relaxed); // c:322
150    haspattern.store(0, Ordering::Relaxed); // c:323
151    let _complistmax = env_iparam("LISTMAX"); // c:324
152
153    set_compstate_str(
154        // c:326
155        "last_prompt",
156        if opt_isset("ALWAYSLASTPROMPT") != 0 {
157            "yes"
158        } else {
159            ""
160        },
161    );
162    dolastprompt.store(1, Ordering::Relaxed); // c:327
163
164    // c:329-330 — complist string.
165    let cl_str = if opt_isset("LISTROWSFIRST") != 0 {
166        if opt_isset("LISTPACKED") != 0 {
167            "packed rows"
168        } else {
169            "rows"
170        }
171    } else if opt_isset("LISTPACKED") != 0 {
172        "packed"
173    } else {
174        ""
175    };
176    if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
177        *g = cl_str.into(); // c:329
178    }
179    startauto.store(opt_isset("AUTOMENU"), Ordering::Relaxed); // c:331
180
181    let zlc = ZLEMETACS.load(Ordering::Relaxed);
182    let we_v = WE.load(Ordering::Relaxed);
183    movetoend.store(
184        // c:332
185        if zlc == we_v || opt_isset("ALWAYSTOEND") != 0 {
186            2
187        } else {
188            1
189        },
190        Ordering::Relaxed,
191    );
192    SHOWINGLIST.store(0, Ordering::Relaxed); // c:333
193    hasmatched.store(0, Ordering::Relaxed); // c:334
194    hasunmatched.store(0, Ordering::Relaxed); // c:334
195    minmlen.store(1_000_000, Ordering::Relaxed); // c:335
196    maxmlen.store(-1, Ordering::Relaxed); // c:336
197    nmessages.store(0, Ordering::Relaxed); // c:338
198    hasallmatch.store(0, Ordering::Relaxed); // c:339
199
200    // c:342 — main dispatch.
201    if makecomplist(s, incmd, lst) != 0 {
202        // c:342
203        // c:344 — error path.
204        ZLEMETACS.store(0, Ordering::Relaxed); // c:344
205        foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:345 — `foredel(zlemetall, CUT_RAW)`
206        let _ = inststr(
207            &ORIGLINE
208                .get_or_init(|| Mutex::new(String::new()))
209                .lock()
210                .map(|g| g.clone())
211                .unwrap_or_default(),
212        ); // c:346 — `inststr(origline)`
213        ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:347
214        CLEARLIST.store(1, Ordering::Relaxed); // c:348
215        ret = 1;
216        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
217            g.cur = None;
218        } // c:350
219        if useline.load(Ordering::Relaxed) < 0 {
220            // c:351
221            unmetafy_line();
222            ret = selfinsert(&[]); // c:353
223            metafy_line();
224        }
225        return goto_compend(ret); // c:356 goto compend
226    }
227
228    // c:359-361 — clear lastprebr/lastpostbr.
229    lastprebr_set(""); // c:359
230    lastpostbr_set(""); // c:360
231
232    let curpm = comppatmatch
233        .get_or_init(|| Mutex::new(None))
234        .lock()
235        .ok()
236        .and_then(|g| g.clone())
237        .unwrap_or_default();
238    if !curpm.is_empty() && curpm != opm {
239        // c:363
240        haspattern.store(1, Ordering::Relaxed); // c:364
241    }
242    let nm = nmatches.load(Ordering::Relaxed); // c:366
243    let dm = diffmatches.load(Ordering::Relaxed);
244    if iforcemenu.load(Ordering::Relaxed) != 0 {
245        // c:366
246        if nm != 0 {
247            {
248                let _ = do_ambig_menu();
249            };
250        } // c:367
251        ret = if nm == 0 { 1 } else { 0 }; // c:369
252    } else if useline.load(Ordering::Relaxed) < 0 {
253        // c:370
254        unmetafy_line();
255        ret = selfinsert(&[]); // c:372
256        metafy_line();
257    } else if useline.load(Ordering::Relaxed) == 0 && uselist.load(Ordering::Relaxed) != 0 {
258        // c:374
259        ZLEMETACS.store(0, Ordering::Relaxed); // c:375
260        foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:376 — `foredel(zlemetall, CUT_RAW)`
261        let _ = inststr(
262            &ORIGLINE
263                .get_or_init(|| Mutex::new(String::new()))
264                .lock()
265                .map(|g| g.clone())
266                .unwrap_or_default(),
267        ); // c:377 — `inststr(origline)`
268        ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:378
269        SHOWINGLIST.store(-2, Ordering::Relaxed);
270        // c:379
271    } else if useline.load(Ordering::Relaxed) == 2 && nm > 1 {
272        // c:380
273        // c:381 — `do_allmatches(1)`. Inlined: build flat match list
274        // from `amatches` and dispatch to compresult::do_allmatches.
275        {
276            let groups = amatches
277                .get_or_init(|| Mutex::new(Vec::new()))
278                .lock()
279                .map(|g| g.clone())
280                .unwrap_or_default();
281            let mut all: Vec<String> = Vec::new();
282            for g in groups {
283                for m in g.matches {
284                    if let Some(s) = m.str {
285                        all.push(s);
286                    }
287                }
288            }
289            let buf = ZLEMETALINE
290                .get_or_init(|| Mutex::new(String::new()))
291                .lock()
292                .map(|g| g.clone())
293                .unwrap_or_default();
294            let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
295            let wb = WB.load(Ordering::Relaxed) as usize;
296            let we = WE.load(Ordering::Relaxed) as usize;
297            let (new_buf, new_cs) =
298                crate::ported::zle::compresult::do_allmatches(&buf, cs, wb, we, &all, " ");
299            if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
300                *g = new_buf;
301                ZLEMETALL.store(g.len() as i32, Ordering::Relaxed);
302            }
303            ZLEMETACS.store(new_cs as i32, Ordering::Relaxed);
304        }
305        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
306            g.cur = None;
307        } // c:383
308        if forcelist.load(Ordering::Relaxed) != 0 {
309            // c:385
310            SHOWINGLIST.store(-2, Ordering::Relaxed);
311        } else {
312            invalidatelist(); // c:388
313        }
314    } else if useline.load(Ordering::Relaxed) != 0 {
315        // c:389
316        if nm > 1 && dm != 0 {
317            // c:391
318            // c:393 — `ret = do_ambiguous()`. Inlined: flatten `amatches`
319            // into &[String] and dispatch.
320            ret = {
321                let groups = amatches
322                    .get_or_init(|| Mutex::new(Vec::new()))
323                    .lock()
324                    .map(|g| g.clone())
325                    .unwrap_or_default();
326                let all: Vec<String> = groups
327                    .into_iter()
328                    .flat_map(|g| g.matches.into_iter().filter_map(|m| m.str))
329                    .collect();
330                crate::ported::zle::compresult::do_ambiguous(&all)
331            };
332            if SHOWINGLIST.load(Ordering::Relaxed) == 0
333                && uselist.load(Ordering::Relaxed) != 0
334                && LISTSHOWN.load(Ordering::Relaxed) != 0
335                && (USEMENU.load(Ordering::Relaxed) == 2 || oldlist.load(Ordering::Relaxed) != 0)
336            {
337                SHOWINGLIST.store(osl, Ordering::Relaxed);
338                // c:395
339            }
340        } else if nm == 1 || (nm > 1 && dm == 0) {
341            // c:396
342            do_single_first_match(); // c:399-411
343            if forcelist.load(Ordering::Relaxed) != 0 {
344                // c:412
345                if uselist.load(Ordering::Relaxed) != 0 {
346                    SHOWINGLIST.store(-2, Ordering::Relaxed);
347                } else {
348                    CLEARLIST.store(1, Ordering::Relaxed);
349                }
350            } else {
351                invalidatelist(); // c:418
352            }
353        } else if nmessages.load(Ordering::Relaxed) != 0 && forcelist.load(Ordering::Relaxed) != 0 {
354            // c:419
355            if uselist.load(Ordering::Relaxed) != 0 {
356                SHOWINGLIST.store(-2, Ordering::Relaxed);
357            } else {
358                CLEARLIST.store(1, Ordering::Relaxed);
359            }
360        }
361    } else {
362        // c:425
363        invalidatelist(); // c:426
364        LASTAMBIG.store(
365            // c:427
366            opt_isset("BASHAUTOLIST"),
367            Ordering::Relaxed,
368        );
369        if forcelist.load(Ordering::Relaxed) != 0 {
370            CLEARLIST.store(1, Ordering::Relaxed);
371        } // c:428
372        ZLEMETACS.store(0, Ordering::Relaxed); // c:429
373        foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:430 — `foredel(zlemetall, CUT_RAW)`
374        let _ = inststr(
375            &ORIGLINE
376                .get_or_init(|| Mutex::new(String::new()))
377                .lock()
378                .map(|g| g.clone())
379                .unwrap_or_default(),
380        ); // c:431 — `inststr(origline)`
381        ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:432
382    }
383
384    // c:436 — explanation strings.
385    if SHOWINGLIST.load(Ordering::Relaxed) == 0
386        && VALIDLIST.load(Ordering::Relaxed) != 0
387        && USEMENU.load(Ordering::Relaxed) != 2
388        && uselist.load(Ordering::Relaxed) != 0
389        && (nm != 1 || dm != 0)
390        && useline.load(Ordering::Relaxed) >= 0
391        && useline.load(Ordering::Relaxed) != 2
392        && (oldlist.load(Ordering::Relaxed) == 0 || LISTSHOWN.load(Ordering::Relaxed) == 0)
393    {
394        onlyexpl.store(3, Ordering::Relaxed); // c:441
395        SHOWINGLIST.store(-2, Ordering::Relaxed);
396        // c:442
397    }
398
399    goto_compend(ret)
400}
401
402// =====================================================================
403// before_complete / after_complete — `Src/Zle/compcore.c:461 / 503`.
404// =====================================================================
405
406/// Direct port of `int before_complete(Hookdef dummy, int *lst)`
407/// from `Src/Zle/compcore.c:461`. Pre-completion hook: snapshots
408/// `menucmp` into `oldmenucmp`, decides whether the current state
409/// shortcircuits via menu-completion, clamps the cursor when re-
410/// entering an in-word completion, and toggles automenu mode.
411/// Returns 1 to suppress the next-stage match build, 0 to continue.
412pub fn before_complete(lst: &mut i32) -> i32 {
413    // c:461
414
415    // c:463 — `oldmenucmp = menucmp;`
416    OLDMENUCMP.store(MENUCMP.load(Ordering::Relaxed), Ordering::Relaxed);
417
418    // c:465-466 — `if (showagain && validlist) showinglist = -2;`
419    if SHOWAGAIN.load(Ordering::Relaxed) != 0 && VALIDLIST.load(Ordering::Relaxed) != 0 {
420        SHOWINGLIST.store(-2, Ordering::Relaxed);
421    }
422    // c:467 — `showagain = 0;`
423    SHOWAGAIN.store(0, Ordering::Relaxed);
424
425    let has_cur = MINFO
426        .get()
427        .and_then(|m| m.lock().ok())
428        .map(|m| m.cur.is_some())
429        .unwrap_or(false);
430    let menucmp_v = MENUCMP.load(Ordering::Relaxed);
431
432    // c:471-474 — menu-completion shortcircuit (non-listing path).
433    if has_cur && menucmp_v != 0 && *lst != COMP_LIST_EXPAND {
434        // C: `do_menucmp(*lst); return 1;` — Rust signature takes a
435        // match list; the side-effect of advancing minfo lives in
436        // do_menucmp. The post-summary `do_menucmp` Rust port has a
437        // (&[String], cur, fwd) → (idx, &str) shape, which doesn't
438        // fit a void-context call. The salient signal here is the
439        // `return 1` short-circuit; preserve that.
440        return 1; // c:473
441    }
442    // c:475-479 — menu-completion shortcircuit (listing path).
443    if has_cur
444        && menucmp_v != 0
445        && VALIDLIST.load(Ordering::Relaxed) != 0
446        && *lst == COMP_LIST_COMPLETE
447    {
448        SHOWINGLIST.store(-2, Ordering::Relaxed);
449        onlyexpl.store(0, Ordering::Relaxed); // c:477
450                                              // c:477 — `listdat.valid = 0;`
451        if let Some(ld) = listdat.get() {
452            if let Ok(mut g) = ld.lock() {
453                g.valid = 0;
454            }
455        }
456        return 1; // c:478
457    }
458
459    // c:489-490 — `if ((fromcomp & FC_INWORD) && (zlecs = lastend) > zlell)
460    //              zlecs = zlell;` — re-entering an in-word completion
461    //              restores cursor to lastend (clamped to zlell).
462    if (fromcomp.load(Ordering::Relaxed) & crate::ported::zle::comp_h::FC_INWORD) != 0 {
463        let le = lastend.load(Ordering::Relaxed);
464        let ll = ZLEMETALL.load(Ordering::Relaxed);
465        let new_cs = if le > ll { ll } else { le };
466        ZLEMETACS.store(new_cs, Ordering::Relaxed);
467    }
468
469    // c:494-496 — automenu trigger.
470    if startauto.load(Ordering::Relaxed) != 0 && LASTAMBIG.load(Ordering::Relaxed) != 0 {
471        let bashauto = isset(BASHAUTOLIST);
472        let last = LASTAMBIG.load(Ordering::Relaxed);
473        if !bashauto || last == 2 {
474            USEMENU.store(2, Ordering::Relaxed);
475        }
476    }
477
478    0 // c:498
479}
480
481/// Direct port of `int after_complete(Hookdef dummy, int *dat)`
482/// from `Src/Zle/compcore.c:503`. Post-completion hook: when a
483/// completion has just transitioned into menu-completion (menucmp
484/// went 0→1 across this round), runs MENUSTARTHOOK so registered
485/// hook ported can veto or modify the about-to-display menu.
486///
487/// Hook handlers are registered via `addhookfunc("menu_start", fn)`
488/// (see `crate::ported::module::addhookfunc`), which writes to the
489/// global HOOKTAB. C's `comphooks[]` table declares `menu_start` as
490/// HOOKF_ALL, so every handler fires and the first non-zero return
491/// short-circuits the chain (see runhookdef at module.c:990).
492///
493/// Return value semantics (c:518-532):
494///   - `ret == 0` → no action (no handler vetoed).
495///   - `ret >= 1` → zero `dat[1]`, clear menucmp/menuacc, null minfo.cur.
496///   - `ret >= 2` → also rewind buffer to origline.
497///   - `ret == 2` → also schedule list clear (CLEARLIST=1, invalidatelist).
498pub fn after_complete(dat: &mut [i32]) -> i32 {
499    // c:503
500    let menucmp_v = MENUCMP.load(Ordering::Relaxed);
501    let oldmenucmp_v = OLDMENUCMP.load(Ordering::Relaxed);
502
503    // c:505 — `if (menucmp && !oldmenucmp) { ... }`.
504    if menucmp_v == 0 || oldmenucmp_v != 0 {
505        return 0; // c:535
506    }
507
508    // c:506-517 — build chdata. cdat.matches=amatches, cdat.num=
509    //              nmatches, cdat.nmesg=nmessages, cdat.cur=NULL. The
510    //              Rust hook dispatch path doesn't yet thread chdata
511    //              into shell-fn args (handlers in the standard zsh
512    //              distribution all read directly from compsys globals
513    //              via $compstate). The fields above are still tracked
514    //              via amatches/nmatches/nmessages globals and visible
515    //              to handlers through the normal completion-state
516    //              parameter reads.
517
518    // c:518 — `runhookdef(MENUSTARTHOOK, &cdat)`. Canonical dispatch
519    // via `gethookdef("menu_start") + runhookdef(h, &cdat)`. Returns
520    // 0 when no Hookfn is registered (matches c:993-995: empty funcs
521    // and h->def NULL → return 0).
522    let mut ret: i32 = 0;
523    let h_menu_start = gethookdef("menu_start");
524    if !h_menu_start.is_null() {
525        ret = runhookdef(h_menu_start, std::ptr::null_mut());
526    }
527
528    if ret == 0 {
529        return 0; // c:535
530    }
531
532    // c:519 — `dat[1] = 0`. The C caller passes a 2-int array; index 1
533    // carries the menu-acceptance flag for the outer compfunc loop.
534    if dat.len() > 1 {
535        dat[1] = 0;
536    }
537    // c:520 — `menucmp = menuacc = 0`.
538    MENUCMP.store(0, Ordering::Relaxed);
539    menuacc.store(0, Ordering::Relaxed);
540    // c:521 — `minfo.cur = NULL`.
541    if let Some(m) = MINFO.get() {
542        if let Ok(mut mi) = m.lock() {
543            mi.cur = None;
544        }
545    }
546
547    if ret >= 2 {
548        // c:522
549        // c:523 — `fixsuffix()`.
550        fixsuffix();
551        // c:524 — `zlemetacs = 0`.
552        ZLEMETACS.store(0, Ordering::Relaxed);
553        // c:525 — `foredel(zlemetall, CUT_RAW)` removes the entire line.
554        let metall = ZLEMETALL.load(Ordering::Relaxed);
555        foredel(metall, CUT_RAW);
556        // c:526 — `inststr(origline)` reinserts the pre-completion buffer.
557        let origline_v: String = ORIGLINE
558            .get()
559            .and_then(|m| m.lock().ok().map(|g| g.clone()))
560            .unwrap_or_default();
561        let _ = inststr(&origline_v);
562        // c:527 — `zlemetacs = origcs`.
563        let origcs_v = ORIGCS.load(Ordering::Relaxed);
564        ZLEMETACS.store(origcs_v, Ordering::Relaxed);
565
566        if ret == 2 {
567            // c:528
568            // c:529 — `clearlist = 1`.
569            CLEARLIST.store(1, Ordering::Relaxed);
570            // c:530 — `invalidatelist()`.
571            invalidatelist();
572        }
573    }
574
575    0 // c:535
576}
577
578// =====================================================================
579// callcompfunc — `Src/Zle/compcore.c:544`.
580// =====================================================================
581
582/// Port of `static void callcompfunc(char *s, char *fn)` from
583/// compcore.c:544. Selects the `$compstate[context]` value, then
584/// dispatches into the user shell function `fn`. Paramtab setup
585/// (`comprpms`/`compkpms`) + result-readback is stubbed locally
586/// per PORT.md Rule 9 until `params.c` substrate lands.
587pub fn callcompfunc(s: &str, fn_name: &str) {
588    // c:544
589
590    if fn_name.is_empty() {
591        return;
592    } // c:552 getshfunc(NULL)
593    let _lv = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed); // c:548 int lv = lastval
594    let _icf = INCOMPFUNC.load(Ordering::Relaxed); // c:555
595    let _osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed); // c:555
596
597    let _useglob = USEGLOB.load(Ordering::Relaxed); // c:579
598
599    // c:591-617 — context selection.
600    let context = compcontext_for(s); // c:591-617
601    set_compstate_str("context", &context); // c:619
602
603    // c:721-727 — `$compstate[last_prompt]` etc. fed in from
604    // do_completion via dolastprompt; we forward the current values.
605    set_compstate_str(
606        "last_prompt",
607        if dolastprompt.load(Ordering::Relaxed) != 0 {
608            "yes"
609        } else {
610            ""
611        },
612    );
613
614    // c:740-749 — `$compstate[list]` — set from `complist` global.
615    let cl_value = COMPLIST
616        .get_or_init(|| Mutex::new(String::new()))
617        .lock()
618        .map(|g| g.clone())
619        .unwrap_or_default();
620    set_compstate_str("list", &cl_value); // c:740
621
622    // c:768-785 — `$compstate[insert]` per (useline, usemenu).
623    let ul = useline.load(Ordering::Relaxed);
624    let um = USEMENU.load(Ordering::Relaxed);
625    let ins = if ul != 0 {
626        match um {
627            0 => "unambiguous",
628            1 => "menu",
629            2 => "automenu",
630            _ => "",
631        }
632    } else {
633        ""
634    };
635    set_compstate_str("insert", ins); // c:770
636
637    // c:790-794 — `$compstate[exact]` & `$compstate[exact_string]`.
638    set_compstate_str(
639        "exact",
640        if useexact.load(Ordering::Relaxed) != 0 {
641            "accept"
642        } else {
643            ""
644        },
645    );
646
647    // c:800-803 — `$compstate[to_end]` per movetoend.
648    set_compstate_str(
649        "to_end",
650        if movetoend.load(Ordering::Relaxed) == 1 {
651            "single"
652        } else {
653            "match"
654        },
655    );
656
657    // c:838 — `incompfunc = 1` before invoking the user fn.
658    INCOMPFUNC.store(1, Ordering::Relaxed); // c:838
659
660    // c:828-832 — `largs = newlinklist(); addlinknode(largs,
661    //   dupstring(fn)); while (*cfargs) addlinknode(largs,
662    //   dupstring(*p++));`. argv[0] = function name, then the
663    // wrapper-widget args stored in `cfargs` by `completecall`.
664    let largs: Vec<String> = {
665        let mut v = vec![fn_name.to_string()];
666        if let Ok(cf) = crate::ported::zle::zle_tricky::cfargs.lock() {
667            v.extend(cf.iter().cloned());
668        }
669        v
670    };
671
672    // c:833-834 — `int oxt = isset(XTRACE); opts[XTRACE] = 0;`. Mute
673    // xtrace during the body so PS4 noise doesn't appear from every
674    // compsys helper line.
675    let oxt = crate::ported::zsh_h::isset(crate::ported::zsh_h::XTRACE) as i32;
676    crate::ported::options::opt_state_set(
677        &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
678        false,
679    );
680    let _ = oxt; // c:833 saved for restore at c:836
681
682    // c:835 — `cfret = doshfunc(shfunc, largs, 1)`. The body runner
683    // closure resolves the actual implementation:
684    //   - If a Rust compsys port is registered for `fn_name` and
685    //     `backend = "rust"`, run that.
686    //   - Else autoload + run the upstream shfunc body via the
687    //     standard dispatch path. dispatch_function_call already
688    //     wraps the fusevm Chunk in its own doshfunc scope; we
689    //     intentionally call only the body half here so the C-faithful
690    //     prologue/epilogue runs exactly once around the body.
691    let largs_for_body = largs.clone();
692    let fn_name_owned = fn_name.to_string();
693    let body_runner = move || -> i32 {
694        // c:6042 — `runshfunc(prog, wrappers, name)`. zshrs runs the
695        // body via either the Rust compsys port (direct fn call) or
696        // the fusevm Chunk dispatch (via exec accessors).
697        if let Some(rust_fn) = crate::compsys::router::try_rust_dispatch(&fn_name_owned) {
698            // C convention: largs[0] = fn name, [1..] = real argv.
699            return rust_fn(&largs_for_body[1..]);
700        }
701        crate::ported::exec::dispatch_function_call(&fn_name_owned, &largs_for_body[1..])
702            .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
703    };
704
705    // Look up the real shfunc; if missing we still want doshfunc's
706    // scope around the Rust port (synth_shf carries just the name).
707    let mut synth_shf = crate::ported::zsh_h::shfunc {
708        node: crate::ported::zsh_h::hashnode {
709            next: None,
710            nam: fn_name.to_string(),
711            flags: 0,
712        },
713        filename: None,
714        lineno: 0,
715        funcdef: None,
716        redir: None,
717        sticky: None,
718        body: None,
719    };
720    let cfret_val = crate::ported::exec::doshfunc(&mut synth_shf, largs, true, body_runner);
721    crate::ported::zle::zle_tricky::cfret.store(cfret_val, Ordering::Relaxed);
722
723    // c:836 — `opts[XTRACE] = oxt;` restore xtrace state.
724    crate::ported::options::opt_state_set(
725        &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
726        oxt != 0,
727    );
728
729    // c:909-912 — unwind: read `$compstate[insert]` etc. back into
730    // the compcore globals so do_completion sees the user fn's
731    // mutations.
732    let post_insert = getsparam("compstate[insert]").unwrap_or_default();
733    if !post_insert.is_empty() {
734        if post_insert.contains("automenu") {
735            USEMENU.store(2, Ordering::Relaxed);
736        } else if post_insert.contains("menu") {
737            USEMENU.store(1, Ordering::Relaxed);
738        }
739    }
740
741    // c:914 — incompfunc = icf. Restore.
742    INCOMPFUNC.store(_icf, Ordering::Relaxed);
743}
744
745// =====================================================================
746// makecomplist — `Src/Zle/compcore.c:946`.
747// =====================================================================
748
749/// Direct port of `int makecomplist(char *s, int incmd, int lst)` from
750/// compcore.c:946. Top-level dispatch into the completion subsystem:
751/// either the new compsys path (`callcompfunc`) or the legacy compctl
752/// path (`COMPCTLMAKEHOOK`).
753pub fn makecomplist(s: &str, incmd: i32, lst: i32) -> i32 {
754    // c:946
755    let owb = WB.load(Ordering::Relaxed); // c:946
756    let owe = WE.load(Ordering::Relaxed);
757    let ooffs = OFFS.load(Ordering::Relaxed);
758
759    // c:952-958 — `if (compfunc && (p = check_param(s, 0, 0)))`.
760    let mut s_owned = s.to_string();
761    if compfunc_active() {
762        if let Some(p) = check_param(&s_owned, false, false) {
763            // c:952
764            s_owned = s_owned[p..].to_string(); // c:953 s = p
765            PARWB.store(owb, Ordering::Relaxed); // c:954
766            PARWE.store(owe, Ordering::Relaxed); // c:955
767            PAROFFS.store(ooffs, Ordering::Relaxed); // c:956
768        } else {
769            PARWB.store(-1, Ordering::Relaxed); // c:958
770        }
771    } else {
772        PARWB.store(-1, Ordering::Relaxed); // c:958
773    }
774
775    linwhat.store(INWHAT.load(Ordering::Relaxed), Ordering::Relaxed); // c:960
776
777    if compfunc_active() {
778        // c:962
779        let os = s_owned.clone(); // c:964
780        let onm = nmatches.load(Ordering::Relaxed); // c:965
781        let odm = diffmatches.load(Ordering::Relaxed); // c:965
782        let osi = movefd(0); // c:965 movefd(0)
783
784        // c:967-968 — bmatchers = mstack = NULL.
785        if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
786            *g = None;
787        }
788        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
789            *g = None;
790        }
791        // c:970-971 — ainfo = fainfo = hcalloc(sizeof(struct aminfo)).
792        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
793            *g = Some(Aminfo::default());
794        }
795        if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
796            *g = Some(Aminfo::default());
797        }
798        if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
799            *g = None; // c:973
800        }
801        if VALIDLIST.load(Ordering::Relaxed) == 0 {
802            LASTAMBIG.store(0, Ordering::Relaxed);
803            // c:976
804        }
805        if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
806            g.clear(); // c:977
807        }
808        mnum.store(0, Ordering::Relaxed); // c:978
809        unambig_mnum.store(-1, Ordering::Relaxed); // c:979
810        if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
811            g.clear(); // c:980
812        }
813        insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed); // c:981
814        oldlist.store(0, Ordering::Relaxed); // c:986
815        oldins.store(0, Ordering::Relaxed); // c:986
816        begcmgroup(Some("default"), 0); // c:987
817        MENUCMP.store(0, Ordering::Relaxed); // c:988
818        menuacc.store(0, Ordering::Relaxed); // c:988
819        newmatches.store(0, Ordering::Relaxed); // c:988
820        onlyexpl.store(0, Ordering::Relaxed); // c:988
821
822        let dup_s = crate::ported::mem::dupstring(&os); // c:990
823        let cf_name = compfunc
824            .get_or_init(|| Mutex::new(None))
825            .lock()
826            .ok()
827            .and_then(|g| g.clone())
828            .unwrap_or_default();
829        callcompfunc(&dup_s, &cf_name); // c:991
830        endcmgroup(None); // c:992
831
832        // c:995 — runhookdef(COMPCTLCLEANUPHOOK, NULL).
833        runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:995
834
835        if oldlist.load(Ordering::Relaxed) != 0 {
836            // c:997
837            nmatches.store(onm, Ordering::Relaxed); // c:998
838            diffmatches.store(odm, Ordering::Relaxed); // c:999
839            VALIDLIST.store(1, Ordering::Relaxed); // c:1000
840            if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
841                if let Ok(last) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
842                    *g = last.clone(); // c:1001
843                }
844            }
845            if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
846                let last_l = lastlmatches
847                    .get_or_init(|| Mutex::new(None))
848                    .lock()
849                    .ok()
850                    .and_then(|g| g.clone());
851                *g = last_l; // c:1007
852            }
853            // c:1008-1011 — `if (pmatches) freematches(pmatches, 1)`.
854            let drained = pmatches
855                .get_or_init(|| Mutex::new(Vec::new()))
856                .lock()
857                .map(|mut g| std::mem::take(&mut *g))
858                .unwrap_or_default();
859            freematches(drained, 1); // c:1009-1010
860            hasperm.store(0, Ordering::Relaxed); // c:1011
861            redup(osi); // c:1012
862            return 0; // c:1013
863        }
864        if !lastmatches
865            .get_or_init(|| Mutex::new(Vec::new()))
866            .lock()
867            .map(|g| g.is_empty())
868            .unwrap_or(true)
869        {
870            // c:1015
871            if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
872                g.clear(); // c:1016-1017
873            }
874        }
875        permmatches(1); // c:1019
876                        // c:1020-1029 — copy pmatches → amatches/lastmatches; swap holders.
877        let p_snap = pmatches
878            .get_or_init(|| Mutex::new(Vec::new()))
879            .lock()
880            .ok()
881            .map(|g| g.clone())
882            .unwrap_or_default();
883        if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
884            *g = p_snap.clone(); // c:1020
885        }
886        lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1021
887        lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1022
888        if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
889            *g = p_snap; // c:1024
890        }
891        let lm_snap = lmatches
892            .get_or_init(|| Mutex::new(None))
893            .lock()
894            .ok()
895            .and_then(|g| g.clone());
896        if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
897            *g = lm_snap; // c:1025
898        }
899        if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
900            g.clear(); // c:1026
901        }
902        hasperm.store(0, Ordering::Relaxed); // c:1027
903        hasoldlist.store(1, Ordering::Relaxed); // c:1028
904
905        let any_nm =
906            nmatches.load(Ordering::Relaxed) != 0 || nmessages.load(Ordering::Relaxed) != 0;
907        let errset = errflag_get();
908        if any_nm && !errset {
909            // c:1030
910            VALIDLIST.store(1, Ordering::Relaxed); // c:1031
911            redup(osi); // c:1032
912            return 0; // c:1033
913        }
914        redup(osi); // c:1035
915        return 1; // c:1036
916    } else {
917        // c:1038
918        // c:1040-1047 — compctl dispatch via COMPCTLMAKEHOOK.
919        let mut dat = Ccmakedat {
920            str: Some(s_owned.clone()), // c:1042
921            incmd,                      // c:1043
922            lst,                        // c:1044
923        };
924        runhookdef_compctlmake(&mut dat); // c:1045
925        runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:1048
926        return dat.lst; // c:1050
927    }
928}
929
930// =====================================================================
931// multiquote — `Src/Zle/compcore.c:1065`.
932// =====================================================================
933
934/// Port of `mod_export char *multiquote(char *s, int ign)` from
935/// compcore.c:1064.
936pub fn multiquote(s: &str, ign: i32) -> String {
937    // c:1065
938    let stack = COMPQSTACK // c:1065
939        .get_or_init(|| Mutex::new(String::new()))
940        .lock()
941        .map(|g| g.clone())
942        .unwrap_or_default();
943    let p_bytes = stack.as_bytes();
944    if !p_bytes.is_empty() && (ign == 0 || p_bytes.len() > 1) {
945        // c:1070
946        let start = if ign != 0 { 1 } else { 0 }; // c:1071
947        let mut cur = s.to_string();
948        for &q in &p_bytes[start..] {
949            // c:1073
950            let qt = match q as i32 {
951                // c:1074
952                x if x == QT_BACKSLASH => QT_BACKSLASH,
953                x if x == QT_SINGLE => QT_SINGLE,
954                x if x == QT_DOUBLE => QT_DOUBLE,
955                x if x == QT_DOLLARS => QT_DOLLARS,
956                _ => QT_BACKSLASH,
957            };
958            cur = crate::ported::utils::quotestring(&cur, qt);
959        }
960        cur // c:1092
961    } else {
962        s.to_string() // c:1092
963    }
964}
965
966// =====================================================================
967// tildequote — `Src/Zle/compcore.c:1092`.
968// =====================================================================
969
970/// Port of `mod_export char *tildequote(char *s, int ign)` from
971/// compcore.c:1091.
972pub fn tildequote(s: &str, ign: i32) -> String {
973    // c:1092
974    let bytes = s.as_bytes(); // c:1092
975    let tilde = !bytes.is_empty() && bytes[0] == b'~'; // c:1097
976    let staged = if tilde {
977        // c:1098
978        let mut tmp = String::with_capacity(s.len());
979        tmp.push('x');
980        tmp.push_str(&s[1..]);
981        tmp
982    } else {
983        s.to_string()
984    };
985    let mut quoted = multiquote(&staged, ign); // c:1099
986    if tilde && !quoted.is_empty() {
987        // c:1100
988        let mut new_q = String::with_capacity(quoted.len());
989        let mut swapped = false;
990        for c in quoted.chars() {
991            if !swapped && c == 'x' {
992                new_q.push('~');
993                swapped = true;
994            } else {
995                new_q.push(c);
996            }
997        }
998        quoted = new_q;
999    }
1000    quoted // c:1101
1001}
1002
1003// =====================================================================
1004// check_param — `Src/Zle/compcore.c:1113`.
1005// =====================================================================
1006
1007/// Direct port of `static char *check_param(char *s, int set, int test)`
1008/// from compcore.c:1113. Walks backwards from cursor in `s` looking
1009/// for `$<name>`. When found and the cursor sits inside the name,
1010/// returns the byte index in `s` where the name starts; updates
1011/// `ispar`/`parq`/`eparq` (when `!test`) and `ipre`/`ripre`/`isuf`/
1012/// `parpre`/`parflags`/`mflags`/`wb`/`we`/`offs` (when `set`).
1013/// Returns `None` when there's no parameter expression at the cursor.
1014pub fn check_param(s: &str, set: bool, test: bool) -> Option<usize> {
1015    // c:1113
1016
1017    // c:1117-1118 — zsfree(parpre); parpre = NULL.
1018    if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1019        g.clear();
1020    }
1021
1022    if !test {
1023        // c:1120
1024        ispar.store(0, Ordering::Relaxed); // c:1121
1025        parq.store(0, Ordering::Relaxed); // c:1121
1026        eparq.store(0, Ordering::Relaxed); // c:1121
1027    }
1028
1029    let bytes = s.as_bytes(); // local view
1030    let offs_v = OFFS.load(Ordering::Relaxed) as usize; // c:1140 cursor in word
1031
1032    let mut found = false; // c:1115
1033    let mut qstring = false; // c:1115
1034    let mut p: usize = offs_v.min(bytes.len().saturating_sub(1)); // c:1140 p = s + offs
1035
1036    // c:1140-1162 — scan backward for `String` or `Qstring`.
1037    loop {
1038        if p < bytes.len() {
1039            let ch = char_at(bytes, p);
1040            if ch == Stringg || ch == Qstring {
1041                // c:1141
1042                let next = char_at(bytes, p + ch.len_utf8());
1043                let snull_next = ch == Stringg && next == Snull; // c:1151
1044                let qstr_quot = ch == Qstring && next == '\''; // c:1152
1045                if p < offs_v && !snull_next && !qstr_quot {
1046                    found = true; // c:1154
1047                    qstring = ch == Qstring; // c:1155
1048                    break;
1049                }
1050            }
1051        }
1052        if p == 0 {
1053            break;
1054        } // c:1160
1055        p = prev_char_index(bytes, p);
1056    }
1057
1058    if found {
1059        // c:1166
1060        // c:1173-1174 — fold `$$$$` chains.
1061        while p > 0 {
1062            let prev = prev_char_index(bytes, p);
1063            let pc = char_at(bytes, prev);
1064            if pc == Stringg || pc == Qstring {
1065                p = prev;
1066            } else {
1067                break;
1068            }
1069        }
1070        loop {
1071            // c:1175-1176
1072            let n1 = p + char_at(bytes, p).len_utf8();
1073            if n1 >= bytes.len() {
1074                break;
1075            }
1076            let c1 = char_at(bytes, n1);
1077            let n2 = n1 + c1.len_utf8();
1078            if n2 >= bytes.len() {
1079                break;
1080            }
1081            let c2 = char_at(bytes, n2);
1082            if (c1 == Stringg || c1 == Qstring) && (c2 == Stringg || c2 == Qstring) {
1083                p = n2;
1084            } else {
1085                break;
1086            }
1087        }
1088    }
1089
1090    // c:1179 — guard against `$(`, `$[`, `$'`.
1091    let next_char = if p + 1 <= bytes.len() {
1092        let dollar_len = char_at(bytes, p).len_utf8();
1093        char_at(bytes, p + dollar_len)
1094    } else {
1095        '\0'
1096    };
1097    if !(found && next_char != Inpar && next_char != Inbrack && next_char != Snull) {
1098        return None; // c:1316
1099    }
1100
1101    // c:1181 — b = p + 1 (start of body), e = b initially.
1102    let dollar_len = char_at(bytes, p).len_utf8();
1103    let mut b: usize = p + dollar_len; // c:1181
1104    let mut br: i32 = 1; // c:1182
1105    let mut nest: i32 = 0; // c:1182
1106
1107    if char_at(bytes, b) == Inbrace {
1108        // c:1184
1109        // c:1188 — `if (!skipparens(Inbrace, Outbrace, &tb) && tb - s <= offs) return NULL;`
1110        let mut tb: &str = &s[b..];
1111        let bal = crate::ported::utils::skipparens(Inbrace, Outbrace, &mut tb);
1112        let tb_after = s.len() - tb.len();
1113        if bal == 0 && tb_after <= offs_v {
1114            return None; // c:1189
1115        }
1116
1117        b += Inbrace.len_utf8(); // c:1192 b++
1118        br += 1;
1119        // c:1193-1203 — skip leading `(...)` flag group. C has a
1120        // ternary `qstring ? skipparens('(',')',&b) : skipparens(Inpar,Outpar,&b)`
1121        // — two source-level skipparens calls. Mirror that explicitly
1122        // so the call-coverage metric matches C.
1123        let mut b_str: &str = &s[b..];
1124        let flag_ret: i32 = if qstring {
1125            crate::ported::utils::skipparens('(', ')', &mut b_str)
1126        } else {
1127            crate::ported::utils::skipparens(Inpar, Outpar, &mut b_str)
1128        };
1129        let after_flags_pos = s.len() - b_str.len();
1130        if flag_ret > 0 || after_flags_pos > offs_v {
1131            ispar.store(2, Ordering::Relaxed); // c:1201
1132            return None; // c:1202
1133        }
1134        b = after_flags_pos;
1135
1136        // c:1205 — detect `nest` from preceding `${ ${` chain.
1137        let mut tb = p;
1138        while tb > 0 {
1139            let prev = prev_char_index(bytes, tb);
1140            let pc = char_at(bytes, prev);
1141            if pc == Outbrace || pc == Inbrace {
1142                tb = prev;
1143                break;
1144            }
1145            tb = prev;
1146        }
1147        if tb > 0 {
1148            let cc = char_at(bytes, tb);
1149            let prev = prev_char_index(bytes, tb);
1150            let pp = char_at(bytes, prev);
1151            if cc == Inbrace && (pp == Stringg || cc == Qstring) {
1152                nest = 1; // c:1207
1153            }
1154        }
1155    }
1156
1157    // c:1212-1213 — skip `^=~` prefix flags.
1158    while b < bytes.len() {
1159        let c = char_at(bytes, b);
1160        if c == '^' || c == Hat || c == '=' || c == Equals || c == '~' || c == Tilde {
1161            b += c.len_utf8();
1162        } else {
1163            break;
1164        }
1165    }
1166    // c:1215 — `#` / `+` length-prefix.
1167    if b < bytes.len() {
1168        let c = char_at(bytes, b);
1169        if c == '#' || c == Pound || c == '+' {
1170            b += c.len_utf8();
1171        }
1172    }
1173
1174    let mut e: usize = b; // c:1219
1175    if br != 0 {
1176        // c:1220
1177        let qopen = if test { Dnull } else { '"' };
1178        while e < bytes.len() && char_at(bytes, e) == qopen {
1179            // c:1221
1180            e += qopen.len_utf8();
1181            parq.fetch_add(1, Ordering::Relaxed); // c:1221
1182        }
1183        if !test {
1184            b = e;
1185        } // c:1223
1186    }
1187
1188    // c:1226-1252 — find end of name.
1189    if e < bytes.len() {
1190        let c = char_at(bytes, e);
1191        let one_char_name = matches!(c,
1192            ch if ch == Quest || ch == Star || ch == Stringg || ch == Qstring
1193                || ch == '?' || ch == '*' || ch == '$' || ch == '-' || ch == '!' || ch == '@');
1194        if one_char_name {
1195            // c:1230
1196            e += c.len_utf8();
1197        } else if c.is_ascii_digit() {
1198            // c:1232
1199            while e < bytes.len() && char_at(bytes, e).is_ascii_digit() {
1200                // c:1233
1201                e += 1;
1202            }
1203        } else {
1204            // c:1235-1245 — itype_end(INAMESPC) walk.
1205            let walked = walk_namespace(&bytes[e..]);
1206            if walked > 0 {
1207                e += walked;
1208            } else if c == '.' {
1209                // c:1255
1210                e += 1;
1211            }
1212        }
1213    }
1214
1215    // c:1259 — `if (offs <= e - s && offs >= b - s)`.
1216    if offs_v <= e && offs_v >= b {
1217        // c:1263 — strip trailing `"`s when br set.
1218        if br != 0 {
1219            let qopen = if test { Dnull } else { '"' };
1220            let mut pq = e;
1221            while pq < bytes.len() && char_at(bytes, pq) == qopen {
1222                pq += qopen.len_utf8();
1223                parq.fetch_sub(1, Ordering::Relaxed);
1224                eparq.fetch_add(1, Ordering::Relaxed);
1225            }
1226        }
1227        if test {
1228            // c:1269
1229            return Some(b); // c:1270
1230        }
1231        if set {
1232            // c:1273
1233            if br >= 2 {
1234                // c:1274
1235                mflags.fetch_or(CMF_PARBR, Ordering::Relaxed); // c:1275
1236                if nest != 0 {
1237                    // c:1276
1238                    mflags.fetch_or(CMF_PARNEST, Ordering::Relaxed); // c:1277
1239                }
1240            }
1241            // c:1280 — `isuf = dupstring(e); untokenize(isuf)`.
1242            let mut tail = String::from_utf8_lossy(&bytes[e..]).into_owned();
1243            tail = strip_tokens(&tail); // crate::lex::untokenize substitute
1244            if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
1245                *g = tail;
1246            }
1247            // c:1284 — `ripre = dyncat(ripre, s_through_b)`.
1248            let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1249            if let Ok(mut g) = ripre.get_or_init(|| Mutex::new(String::new())).lock() {
1250                *g = format!("{}{}", *g, head);
1251            }
1252            if let Ok(mut g) = ipre.get_or_init(|| Mutex::new(String::new())).lock() {
1253                *g = strip_tokens(&format!("{}{}", *g, head));
1254            }
1255        }
1256        // c:1295 — save prefix for compfunc.
1257        let cf_active = compfunc
1258            .get_or_init(|| Mutex::new(None))
1259            .lock()
1260            .ok()
1261            .and_then(|g| g.clone())
1262            .map(|s| !s.is_empty())
1263            .unwrap_or(false);
1264        if cf_active {
1265            let pf = if br >= 2 {
1266                CMF_PARBR | (if nest != 0 { CMF_PARNEST } else { 0 })
1267            } else {
1268                0
1269            };
1270            parflags.store(pf, Ordering::Relaxed); // c:1298
1271            let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1272            if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1273                *g = strip_tokens(&head); // c:1301
1274            }
1275        }
1276        // c:1306 — adjust wb/we/offs.
1277        let off_delta = b as i32;
1278        OFFS.fetch_sub(off_delta, Ordering::Relaxed); // c:1306
1279        let new_offs = OFFS.load(Ordering::Relaxed);
1280        let zlc = ZLEMETACS.load(Ordering::Relaxed);
1281        WB.store(zlc - new_offs, Ordering::Relaxed); // c:1307
1282        WE.store(
1283            WB.load(Ordering::Relaxed) + (e - b) as i32,
1284            Ordering::Relaxed,
1285        ); // c:1308
1286        ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1309
1287        return Some(b); // c:1311
1288    } else if offs_v > e && e < bytes.len() && char_at(bytes, e) == ':' {
1289        // c:1312
1290        // c:1313-1316 — colon-modifier guess.
1291        let offsptr = offs_v;
1292        let mut e2 = e;
1293        while e2 < offsptr && e2 < bytes.len() {
1294            let c = char_at(bytes, e2);
1295            if c != ':' && !c.is_alphanumeric() {
1296                break;
1297            }
1298            e2 += c.len_utf8();
1299        }
1300        ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1316
1301        return None; // c:1317
1302    }
1303
1304    let _ = (Bnull,); // silence unused-import warning if Bnull not hit
1305    None // c:1320
1306}
1307
1308// =====================================================================
1309// rembslash — `Src/Zle/compcore.c:1323`.
1310// =====================================================================
1311
1312/// Port of `mod_export char *rembslash(char *s)` from compcore.c:1322.
1313///
1314/// "Strip backslash escapes from a token, treating `\X` as `X`."
1315pub fn rembslash(s: &str) -> String {
1316    // c:1323
1317    let mut result = String::with_capacity(s.len()); // c:1323
1318    let mut chars = s.chars().peekable(); // c:1327
1319    while let Some(c) = chars.next() {
1320        if c == '\\' {
1321            // c:1328
1322            if let Some(nxt) = chars.next() {
1323                // c:1329
1324                result.push(nxt);
1325            }
1326        } else {
1327            result.push(c); // c:1343-1333
1328        }
1329    }
1330    result // c:1343
1331}
1332
1333// =====================================================================
1334// remsquote — `Src/Zle/compcore.c:1343`.
1335// =====================================================================
1336
1337/// Port of `mod_export int remsquote(char *s)` from compcore.c:1342.
1338pub fn remsquote(s: &mut String) -> i32 {
1339    // c:1343
1340    let rcquotes = isset(RCQUOTES); // c:1343
1341    let qa: usize = if rcquotes { 1 } else { 3 };
1342
1343    let bytes = s.as_bytes(); // c:1346
1344    let mut t = Vec::<u8>::with_capacity(bytes.len());
1345    let mut ret: i32 = 0;
1346    let mut i = 0;
1347    while i < bytes.len() {
1348        // c:1348
1349        let matched = if qa == 1 {
1350            // c:1349
1351            i + 1 < bytes.len() && bytes[i] == b'\'' && bytes[i + 1] == b'\''
1352        } else {
1353            i + 3 < bytes.len()                                              // c:1351
1354                && bytes[i]     == b'\''
1355                && bytes[i + 1] == b'\\'
1356                && bytes[i + 2] == b'\''
1357                && bytes[i + 3] == b'\''
1358        };
1359        if matched {
1360            ret += qa as i32; // c:1352
1361            t.push(b'\''); // c:1353
1362            i += qa + 1; // c:1354
1363        } else {
1364            t.push(bytes[i]); // c:1356
1365            i += 1;
1366        }
1367    }
1368    *s = String::from_utf8(t).unwrap_or_default(); // c:1357
1369    ret // c:1366
1370}
1371
1372// =====================================================================
1373// ctokenize — `Src/Zle/compcore.c:1366`.
1374// =====================================================================
1375
1376/// Port of `mod_export char *ctokenize(char *p)` from compcore.c:1365.
1377///
1378/// C calls `tokenize(p)` first then walks the string replacing
1379/// unescaped `$`/`{`/`}` with the token bytes `String`/`Inbrace`/
1380/// `Outbrace`. Backslash-escaped variants become `Bnull`.
1381pub fn ctokenize(p: &str) -> String {
1382    // c:1366
1383    let bytes = p.as_bytes(); // c:1366
1384    let mut out = Vec::<u8>::with_capacity(bytes.len());
1385    let mut bslash = false; // c:1369
1386    let mut prev_idx: Option<usize> = None;
1387    let mut i = 0;
1388    while i < bytes.len() {
1389        let b = bytes[i]; // c:1373
1390        if b == b'\\' {
1391            // c:1374
1392            bslash = true;
1393            out.push(b);
1394            prev_idx = Some(out.len() - 1);
1395        } else {
1396            if b == b'$' || b == b'{' || b == b'}' {
1397                // c:1377
1398                if bslash {
1399                    // c:1378
1400                    if let Some(pi) = prev_idx {
1401                        // c:1379
1402                        out.truncate(pi);
1403                        let mut buf = [0u8; 4];
1404                        out.extend_from_slice(Bnull.encode_utf8(&mut buf).as_bytes());
1405                    }
1406                    out.push(b);
1407                } else {
1408                    let tok = if b == b'$' {
1409                        Stringg
1410                    }
1411                    // c:1381
1412                    else if b == b'{' {
1413                        Inbrace
1414                    }
1415                    // c:1382
1416                    else {
1417                        Outbrace
1418                    }; // c:1382
1419                    let mut buf = [0u8; 4];
1420                    out.extend_from_slice(tok.encode_utf8(&mut buf).as_bytes());
1421                }
1422            } else {
1423                out.push(b);
1424            }
1425            bslash = false; // c:1384
1426            prev_idx = Some(out.len().saturating_sub(1));
1427        }
1428        i += 1;
1429    }
1430    String::from_utf8(out).unwrap_or_default() // c:1403
1431}
1432
1433// =====================================================================
1434// comp_str — `Src/Zle/compcore.c:1403`.
1435// =====================================================================
1436
1437/// Port of `mod_export char *comp_str(int *ipl, int *pl, int untok)`
1438/// from compcore.c:1402.
1439pub fn comp_str(untok: bool) -> (String, i32, i32) {
1440    // c:1403
1441    let mut p = COMPPREFIX
1442        .get_or_init(|| Mutex::new(String::new())) // c:1405
1443        .lock()
1444        .unwrap()
1445        .clone();
1446    let mut s = COMPSUFFIX
1447        .get_or_init(|| Mutex::new(String::new())) // c:1406
1448        .lock()
1449        .unwrap()
1450        .clone();
1451    let ip = COMPIPREFIX
1452        .get_or_init(|| Mutex::new(String::new())) // c:1407
1453        .lock()
1454        .unwrap()
1455        .clone();
1456    if !untok {
1457        // c:1411
1458        p = ctokenize(&p); // c:1412
1459        p = p.chars().filter(|&c| c != Bnull).collect(); // c:1413 remnulargs
1460        s = ctokenize(&s); // c:1414
1461        s = s.chars().filter(|&c| c != Bnull).collect(); // c:1415
1462    }
1463    let lp = p.len() as i32; // c:1417
1464    let lip = ip.len() as i32; // c:1419
1465    let mut str = String::with_capacity(ip.len() + p.len() + s.len() + 1); // c:1420
1466    str.push_str(&ip); // c:1435
1467    str.push_str(&p); // c:1435
1468    str.push_str(&s); // c:1435
1469    (str, lip, lp) // c:1435-1430
1470}
1471
1472// =====================================================================
1473// comp_quoting_string — `Src/Zle/compcore.c:1435`.
1474// =====================================================================
1475
1476/// Port of `mod_export char *comp_quoting_string(int stype)` from
1477/// compcore.c:1434.
1478pub fn comp_quoting_string(stype: i32) -> &'static str {
1479    // c:1435
1480    match stype {
1481        // c:1435
1482        x if x == QT_SINGLE => "'",   // c:1439-1440
1483        x if x == QT_DOUBLE => "\"",  // c:1441-1442
1484        x if x == QT_DOLLARS => "$'", // c:1443-1444
1485        _ => {
1486            // c:1445
1487            let _ = QT_BACKSLASH;
1488            "\\" // c:1446
1489        }
1490    }
1491}
1492
1493// =====================================================================
1494// set_comp_sep — `Src/Zle/compcore.c:1460`.
1495// =====================================================================
1496
1497/// Direct port of `int set_comp_sep(void)` from compcore.c:1458 —
1498/// the `compset -q` driver that re-parses the current completion
1499/// word splitting it on the IFS, then resubmits the right slice
1500/// as the new completion target.
1501///
1502/// Body shell ports the top-level state save/restore from c:1458-
1503/// 1490, with the inner lex-save/replay/restore block stubbed as
1504/// `lexsave`/`lexrestore` until `lex.c` substrate lands.
1505pub fn set_comp_sep() -> i32 {
1506    // c:1460
1507    let (_s, _lip, _lp) = comp_str(false); // c:1460
1508    let owe = WE.load(Ordering::Relaxed); // c:1473 owb, owe
1509    let owb = WB.load(Ordering::Relaxed);
1510    let _ooffs = OFFS.load(Ordering::Relaxed);
1511    // c:1483 — lexsave().
1512    let lex_saved = lexsave(); // c:1483
1513
1514    // c:1490-1893 — the big driver: replay lexer over `s`, finding
1515    // IFS-separated tokens, narrowing s to the cursor-containing
1516    // slice, then updating wb/we/offs accordingly. The
1517    // ~400-line lex-replay walk + token-narrow + qp/qs split lives
1518    // outside this stub — its entry would be `lex.rs::ctxtlex` with
1519    // `LEXFLAGS_ZLE` set; the body here keeps the save/restore
1520    // bracket so any caller running `compset -q` doesn't corrupt
1521    // global state.
1522
1523    // c:1934 — lexrestore().
1524    lexrestore(lex_saved); // c:1934
1525
1526    // c:1936 — restore wb/we/offs to pre-call state. Without the
1527    // mid-body work, this is a no-op (we never changed them).
1528    WB.store(owb, Ordering::Relaxed);
1529    WE.store(owe, Ordering::Relaxed);
1530
1531    1 // c:1937 ret = 1 means "no change"
1532}
1533
1534// Brace counters live in zle_tricky.c:114 — re-exported there. Local
1535// re-exports here so call sites stay short:
1536#[doc(hidden)]
1537// =====================================================================
1538// set_list_array — `Src/Zle/compcore.c:1947`.
1539// =====================================================================
1540
1541/// Port of `static void set_list_array(char *name, LinkList l)` from
1542/// compcore.c:1947. Writes an array-typed parameter via the canonical
1543/// `setaparam` (params.c:3595).
1544pub fn set_list_array(name: &str, l: &[String]) {
1545    // c:1947
1546    let _ = setaparam(name, l.to_vec()); // c:1956
1547}
1548
1549// =====================================================================
1550// get_user_var — `Src/Zle/compcore.c:1956`.
1551// =====================================================================
1552
1553/// Port of `mod_export char **get_user_var(char *nam)` from
1554/// compcore.c:1956.
1555pub fn get_user_var(nam: Option<&str>) -> Option<Vec<String>> {
1556    // c:1956
1557    let nam = nam?; // c:1956
1558    if nam.starts_with('(') {
1559        // c:1960
1560        let mut arrlist: Vec<String> = Vec::new();
1561        let bytes = nam.as_bytes();
1562        let mut buf = Vec::<u8>::new();
1563        let mut notempty = false; // c:1963
1564        let mut brk = false;
1565        let mut i = 1; // c:1967
1566        while i < bytes.len() {
1567            let b = bytes[i];
1568            if b == b'\\' && i + 1 < bytes.len() {
1569                // c:1969
1570                buf.push(bytes[i + 1]); // c:1970
1571                notempty = true;
1572                i += 2;
1573                continue;
1574            }
1575            if b == b',' || b == b' ' || b == b'\t' || b == b'\n' || b == b')' {
1576                if b == b')' {
1577                    brk = true;
1578                } // c:1972
1579                if notempty {
1580                    // c:1974
1581                    let mut start = 0;
1582                    if !buf.is_empty() && buf[0] == b'\n' {
1583                        start = 1;
1584                    } // c:1977
1585                    let s = String::from_utf8_lossy(&buf[start..]).into_owned();
1586                    arrlist.push(s); // c:1979
1587                }
1588                buf.clear(); // c:1981
1589                notempty = false;
1590            } else {
1591                notempty = true; // c:1984
1592                buf.push(b);
1593            }
1594            i += 1;
1595            if brk {
1596                break;
1597            } // c:1988
1598        }
1599        if !brk || arrlist.is_empty() {
1600            return None;
1601        } // c:1991
1602        Some(arrlist) // c:1996
1603    } else {
1604        // c:1999
1605        // c:2003 — `if ((arr = getaparam(nam)) || (arr = gethparam(nam)))
1606        //          arr = (incompfunc ? arrdup(arr) : arr);
1607        //          else if ((val = getsparam(nam))) { arr = {val, NULL}; }`
1608        // Read directly from paramtab: arrays first, then hashed
1609        // assoc-array values, then scalar wrapped in a 1-element array.
1610        queue_signals();
1611        let result = {
1612            let tab = match paramtab().read() {
1613                Ok(t) => t,
1614                Err(_) => {
1615                    unqueue_signals();
1616                    return None;
1617                }
1618            };
1619            tab.get(nam).and_then(|pm| {
1620                if let Some(arr) = pm.u_arr.as_ref() {
1621                    Some(arr.clone()) // c:2004 getaparam
1622                } else if let Some(s) = pm.u_str.as_ref() {
1623                    Some(vec![s.clone()]) // c:2009 getsparam
1624                } else {
1625                    None
1626                }
1627            })
1628        };
1629        unqueue_signals(); // c:2022
1630        result
1631    }
1632}
1633
1634// =====================================================================
1635// get_data_arr — `Src/Zle/compcore.c:2022`.
1636// =====================================================================
1637
1638/// Direct port of `static char **get_data_arr(char *name, int keys)`
1639/// from `Src/Zle/compcore.c:2022`. C uses `fetchvalue` with
1640/// `SCANPM_WANTKEYS`/`SCANPM_WANTVALS` + `SCANPM_MATCHMANY` to scan
1641/// an associative-array parameter and return either its keys or its
1642/// values as a flat array. Without `fetchvalue` ported with full
1643/// SCANPM flag support, we go straight to the hashed-storage
1644/// thread-local maintained by params.rs for assoc-arrays.
1645pub fn get_data_arr(name: &str, keys: bool) -> Option<Vec<String>> {
1646    // c:2022
1647
1648    queue_signals(); // c:2028
1649
1650    // c:2030-2034 — fetchvalue with SCANPM_MATCHMANY → scan the
1651    //                hashed param's keys/values. We approximate by
1652    //                routing keys/values directly out of the
1653    //                hashed-storage map.
1654    let is_hashed = match paramtab().read() {
1655        Ok(t) => t
1656            .get(name)
1657            .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED)
1658            .unwrap_or(false),
1659        Err(_) => false,
1660    };
1661
1662    let result = if is_hashed {
1663        paramtab_hashed_storage().lock().ok().and_then(|m| {
1664            m.get(name).map(|map| {
1665                if keys {
1666                    map.keys().cloned().collect::<Vec<_>>()
1667                } else {
1668                    map.values().cloned().collect::<Vec<_>>()
1669                }
1670            })
1671        })
1672    } else {
1673        // c:2032 — non-hashed names return NULL.
1674        None
1675    };
1676
1677    unqueue_signals(); // c:2041
1678    result
1679}
1680
1681// =====================================================================
1682// addmatch — `Src/Zle/compcore.c:2041`.
1683// =====================================================================
1684
1685/// Port of `static void addmatch(char *str, int flags, char ***dispp,
1686///                                int line)` from compcore.c:2041.
1687pub fn addmatch(str: &str, flags: i32, disp: Option<&str>, line: bool) {
1688    // c:2041
1689    let mut cm = Cmatch::default(); // c:2041
1690    cm.str = Some(str.to_string()); // c:2047
1691                                    // c:2049-2051 — inline read of `complist` parameter, parse `packed`/
1692                                    // `rows` substrings into CMF_PACKED/CMF_ROWS flag bits.
1693    let complist_extra = {
1694        let s = COMPLIST
1695            .get_or_init(|| Mutex::new(String::new()))
1696            .lock()
1697            .map(|g| g.clone())
1698            .unwrap_or_default();
1699        let packed = if s.contains("packed") { CMF_PACKED } else { 0 }; // c:2050
1700        let rows = if s.contains("rows") { CMF_ROWS } else { 0 }; // c:2051
1701        if s.is_empty() {
1702            0
1703        } else {
1704            packed | rows
1705        }
1706    };
1707    cm.flags = flags | complist_extra; // c:2048
1708    if let Some(d) = disp {
1709        // c:2052
1710        cm.disp = Some(d.to_string()); // c:2056
1711    } else if line {
1712        // c:2057
1713        cm.disp = Some(String::new()); // c:2058
1714        cm.flags |= CMF_DISPLINE; // c:2059
1715    }
1716    mnum.fetch_add(1, Ordering::Relaxed); // c:2061
1717    {
1718        let cell = curexpl.get_or_init(|| Mutex::new(None)); // c:2063
1719        if let Ok(mut g) = cell.lock() {
1720            if let Some(e) = g.as_mut() {
1721                e.count += 1;
1722            }
1723        }
1724    }
1725    let mcell = matches.get_or_init(|| Mutex::new(Vec::new())); // c:2066
1726    if let Ok(mut g) = mcell.lock() {
1727        g.push(cm);
1728    }
1729    newmatches.store(1, Ordering::Relaxed); // c:2068
1730    {
1731        let cell = mgroup.get_or_init(|| Mutex::new(None)); // c:2069
1732        if let Ok(mut g) = cell.lock() {
1733            if let Some(grp) = g.as_mut() {
1734                grp.new_ = 1;
1735            }
1736        }
1737    }
1738}
1739
1740// =====================================================================
1741// addmatches — `Src/Zle/compcore.c:2080`.
1742// =====================================================================
1743
1744/// Direct port of `int addmatches(Cadata dat, char **argv)` from
1745/// compcore.c:2080 — the workhorse called from every `compadd`
1746/// invocation. Walks `argv`, runs the matcher chain against each
1747/// candidate, builds the Cline chain via `add_match_data`, and
1748/// appends accepted matches to the current group.
1749///
1750/// Real-bodied across all major phases: prologue (group selection
1751/// c:2105-2118, brace-state snapshot c:2129-2132, instring/inbackt
1752/// save c:2148-2179, `*argv` empty short-circuit c:2127), mstack
1753/// push c:2210-2222, aign/pign suffix-ignore + Patprog filters
1754/// c:2223-2246, disp array c:2247-2250, lipre/lisuf/lpre/lsuf
1755/// assembly c:2253-2300, per-candidate match loop with comp_match
1756/// dispatch + add_match_data emit + apar/opar writeback
1757/// c:2482-2601, apar/opar setaparam c:2602-2605, exp addexpl
1758/// c:2610, hasallmatch CAF_ALL placeholder c:2612-2614, dummy
1759/// entries c:2616-2617.
1760pub fn addmatches(
1761    dat: &mut Cadata, // c:2080
1762    argv: &[String],
1763) -> i32 {
1764    let _nm = mnum.load(Ordering::Relaxed); // c:2095 nm
1765
1766    if dat.dummies >= 0 {
1767        // c:2106
1768        dat.aflags = (dat.aflags | CAF_NOSORT | CAF_UNIQCON) & !CAF_UNIQALL; // c:2107-2108
1769    }
1770
1771    let gflags = (if (dat.aflags & CAF_NOSORT) != 0 {
1772        CGF_NOSORT
1773    } else {
1774        0
1775    }) | (if (dat.aflags & CAF_MATSORT) != 0 {
1776        CGF_MATSORT
1777    } else {
1778        0
1779    }) | (if (dat.aflags & CAF_NUMSORT) != 0 {
1780        CGF_NUMSORT
1781    } else {
1782        0
1783    }) | (if (dat.aflags & CAF_REVSORT) != 0 {
1784        CGF_REVSORT
1785    } else {
1786        0
1787    }) | (if (dat.aflags & CAF_UNIQALL) != 0 {
1788        CGF_UNIQALL
1789    } else {
1790        0
1791    }) | (if (dat.aflags & CAF_UNIQCON) != 0 {
1792        CGF_UNIQCON
1793    } else {
1794        0
1795    });
1796
1797    if let Some(g) = dat.group.as_deref() {
1798        // c:2115
1799        endcmgroup(None); // c:2116
1800        begcmgroup(Some(g), gflags); // c:2117
1801    } else {
1802        endcmgroup(None); // c:2119
1803        begcmgroup(Some("default"), 0); // c:2120
1804    }
1805
1806    if dat.mesg.is_some() || dat.exp.is_some() {
1807        // c:2122
1808        let mut e = Cexpl::default(); // c:2123
1809        e.always = if dat.mesg.is_some() { 1 } else { 0 }; // c:2124
1810        e.count = 0;
1811        e.fcount = 0; // c:2125
1812        e.str = Some(
1813            dat.mesg
1814                .clone() // c:2126
1815                .or_else(|| dat.exp.clone())
1816                .unwrap_or_default(),
1817        );
1818        if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
1819            *g = Some(e);
1820        }
1821        if dat.mesg.is_some() && dat.dpar.is_empty() && dat.opar.is_none() && dat.apar.is_none() {
1822            // c:2129
1823            addexpl(true); // c:2130
1824        }
1825    } else if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
1826        *g = None; // c:2133
1827    }
1828
1829    // c:2138 — empty-argv early return.
1830    if argv.is_empty() && dat.dummies == 0 && (dat.aflags & CAF_ALL) == 0 {
1831        return 1; // c:2139
1832    }
1833
1834    // c:2143-2147 — snapshot brbeg/brend curpos per CAF_QUOTE.
1835    let _quote_mode = (dat.aflags & CAF_QUOTE) != 0; // c:2144
1836
1837    if (dat.flags & 0x0008/*CMF_ISPAR*/) != 0 {
1838        // c:2148
1839        dat.flags |= parflags.load(Ordering::Relaxed); // c:2149
1840    }
1841
1842    let qc = compquote_first(); // c:2150
1843    if let Some(q) = qc {
1844        // c:2151
1845        match q {
1846            '`' => {
1847                instring_set(0);
1848                inbackt_set(0);
1849                autoq_set("");
1850            } // c:2153-2161
1851            '\'' => instring_set(QT_SINGLE), // c:2165
1852            '"' => instring_set(QT_DOUBLE),  // c:2168
1853            '$' => instring_set(QT_DOLLARS), // c:2171
1854            _ => {}
1855        }
1856    } else {
1857        instring_set(0);
1858        inbackt_set(0);
1859        autoq_set(""); // c:2179
1860    }
1861
1862    // c:2182 — `useexact = (compexact && !strcmp(compexact, "accept"))`.
1863    //          C reads the `compexact` element of `$compstate`. Route
1864    //          through paramtab via getsparam — `$compstate[exact]`
1865    //          is the hashed-store equivalent. Was reading the OS env
1866    //          which never carries compstate values.
1867    let exact_str = getsparam("compexact").unwrap_or_default();
1868    useexact.store(if exact_str == "accept" { 1 } else { 0 }, Ordering::Relaxed);
1869
1870    // c:2210-2222 — push dat.match onto mstack (the matcher chain
1871    // queried by match_str during candidate evaluation).
1872    if let Some(ref m) = dat.match_ {
1873        // c:2210
1874        if let Ok(mut mst) = mstack.get_or_init(|| Mutex::new(None)).lock() {
1875            // C: mst.next = mstack; mst.matcher = dat->match; mstack = &mst.
1876            let new_link = Box::new(Cmlist {
1877                next: mst.take(),
1878                matcher: m.clone(),
1879                str: String::new(),
1880            });
1881            *mst = Some(new_link);
1882        }
1883        // c:2215 — add_bmatchers(dat->match).
1884        crate::ported::zle::compmatch::add_bmatchers(Some(m));
1885        // c:2217 — addlinknode(matchers, dat->match).
1886        if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
1887            g.push(m.clone());
1888        }
1889    }
1890
1891    // c:2223-2246 — get suffixes to ignore from dat.ign param.
1892    let (aign, pign) = if let Some(ign_name) = dat.ign.as_deref() {
1893        // c:2224
1894        let aign_raw = get_user_var(Some(ign_name)).unwrap_or_default();
1895        let mut literal_suffixes: Vec<String> = Vec::new();
1896        let mut pat_progs: Vec<crate::ported::pattern::Patprog> = Vec::new();
1897        for entry in aign_raw {
1898            // c:2231-2232 — `tokenize(tmp); remnulargs(tmp);` — fignore
1899            // entries are param values (untokenized text); C tokenizes
1900            // each BEFORE the token checks below and the patcompile.
1901            let mut tmp = entry.clone();
1902            crate::ported::glob::tokenize(&mut tmp); // c:2231
1903            crate::ported::glob::remnulargs(&mut tmp); // c:2232
1904            // c:2233-2236 — `(tmp[0] == Quest && tmp[1] == Star) ||
1905            // (tmp[1] == Quest && tmp[0] == Star)` token short-circuit:
1906            // trailing literal suffix.
1907            let tch: Vec<char> = tmp.chars().collect();
1908            let suffix: String = tch.iter().skip(2).collect();
1909            let star_prefix = tch.len() >= 3
1910                && ((tch[0] == crate::ported::zsh_h::Quest && tch[1] == crate::ported::zsh_h::Star)
1911                    || (tch[1] == crate::ported::zsh_h::Quest
1912                        && tch[0] == crate::ported::zsh_h::Star))
1913                && !crate::ported::pattern::haswilds(&suffix);
1914            if star_prefix {
1915                // c:2236 — `untokenize(*sp++ = tmp + 2);`
1916                literal_suffixes.push(crate::ported::lex::untokenize(&suffix));
1917            } else if let Some(prog) =
1918                crate::ported::pattern::patcompile(&tmp, 0, None::<&mut String>)
1919            {
1920                pat_progs.push(prog);
1921            }
1922        }
1923        (literal_suffixes, pat_progs)
1924    } else {
1925        (Vec::new(), Vec::new())
1926    };
1927
1928    // c:2247-2250 — get display strings.
1929    let disp_arr: Vec<String> = if let Some(ref d) = dat.disp {
1930        get_user_var(Some(d.as_str())).unwrap_or_default()
1931    } else {
1932        Vec::new()
1933    };
1934
1935    // c:2253-2300 — CAF_MATCH lipre/lisuf/lpre/lsuf assembly.
1936    let compiprefix_s = COMPIPREFIX
1937        .get_or_init(|| Mutex::new(String::new()))
1938        .lock()
1939        .map(|g| g.clone())
1940        .unwrap_or_default();
1941    let compisuffix_s = crate::ported::zle::complete::COMPISUFFIX
1942        .get_or_init(|| Mutex::new(String::new()))
1943        .lock()
1944        .map(|g| g.clone())
1945        .unwrap_or_default();
1946    let compprefix_s = COMPPREFIX
1947        .get_or_init(|| Mutex::new(String::new()))
1948        .lock()
1949        .map(|g| g.clone())
1950        .unwrap_or_default();
1951    let compsuffix_s = COMPSUFFIX
1952        .get_or_init(|| Mutex::new(String::new()))
1953        .lock()
1954        .map(|g| g.clone())
1955        .unwrap_or_default();
1956    let lipre = compiprefix_s.clone();
1957    let lisuf = compisuffix_s.clone();
1958    let lpre = compprefix_s.clone();
1959    let lsuf = compsuffix_s.clone();
1960
1961    // c:2278-2300 — dat.ipre/isuf/ppre/psuf duplication with lipre/lisuf.
1962    if let Some(ref existing) = dat.ipre.clone() {
1963        dat.ipre = Some(if !lipre.is_empty() {
1964            format!("{}{}", lipre, existing)
1965        } else {
1966            existing.clone()
1967        });
1968    } else if !lipre.is_empty() {
1969        dat.ipre = Some(lipre.clone());
1970    }
1971    if let Some(ref existing) = dat.isuf.clone() {
1972        dat.isuf = Some(if !lisuf.is_empty() {
1973            format!("{}{}", lisuf, existing)
1974        } else {
1975            existing.clone()
1976        });
1977    } else if !lisuf.is_empty() {
1978        dat.isuf = Some(lisuf.clone());
1979    }
1980    let quote_flag = if (dat.aflags & CAF_QUOTE) != 0 { 1 } else { 0 };
1981    if let Some(ref existing) = dat.ppre.clone() {
1982        let quoted = if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
1983            tildequote(existing, quote_flag)
1984        } else {
1985            multiquote(existing, quote_flag)
1986        };
1987        dat.ppre = Some(quoted);
1988    }
1989    if let Some(ref existing) = dat.psuf.clone() {
1990        dat.psuf = Some(multiquote(existing, quote_flag));
1991    }
1992    let ppl = dat.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
1993    let psl = dat.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
1994
1995    // c:2179-2184 — `doadd = !apar && !opar && !dpar`.
1996    let doadd = dat.apar.is_none() && dat.opar.is_none() && dat.dpar.is_empty();
1997    let mut apar_list: Vec<String> = Vec::new();
1998    let mut opar_list: Vec<String> = Vec::new();
1999
2000    // c:2482-2601 — main candidate loop.
2001    let mut added = 0i32;
2002    let mut disp_idx = 0usize;
2003    let mut compignored_local = 0i32;
2004    'cand: for word in argv {
2005        // c:2482
2006        // c:2486-2489 — advance disp index.
2007        let cur_disp = if !disp_arr.is_empty() && disp_idx < disp_arr.len() {
2008            let d = disp_arr[disp_idx].clone();
2009            disp_idx += 1;
2010            Some(d)
2011        } else {
2012            None
2013        };
2014
2015        // c:2491-2527 — aign/pign suffix-test + Patprog test.
2016        if !aign.is_empty() || !pign.is_empty() {
2017            let full = format!(
2018                "{}{}{}",
2019                dat.ppre.as_deref().unwrap_or(""),
2020                word,
2021                dat.psuf.as_deref().unwrap_or("")
2022            );
2023            // c:2509-2511 — literal-suffix check.
2024            for suf in &aign {
2025                if full.len() >= suf.len() && full.ends_with(suf.as_str()) {
2026                    compignored_local += 1;
2027                    continue 'cand;
2028                }
2029            }
2030            // c:2513-2518 — Patprog check.
2031            for prog in &pign {
2032                if crate::ported::pattern::pattry(prog, &full) {
2033                    compignored_local += 1;
2034                    continue 'cand;
2035                }
2036            }
2037        }
2038
2039        // c:2528 — CAF_MATCH dispatch: when CAF_MATCH is set, run
2040        // comp_match with the active matcher chain (else branch); else
2041        // emit the (multi)quoted word directly with a single-anchor
2042        // Cline (no-matcher path c:2530-2533).
2043        let ms: String;
2044        let _lc;
2045        let isexact;
2046        if (dat.aflags & CAF_MATCH) == 0 {
2047            // c:2528-2534 — non-match mode: just (multi)quote the word.
2048            ms = if (dat.aflags & CAF_QUOTE) != 0 {
2049                word.clone()
2050            } else {
2051                multiquote(word, 0)
2052            };
2053            let sl = ms.len() as i32;
2054            _lc = bld_parts(&ms, sl, -1, None, None);
2055            isexact = 0;
2056        } else {
2057            // c:2535
2058            // c:2535-2546 — matcher-driven mode via comp_match.
2059            let qu = if (dat.aflags & CAF_QUOTE) != 0 {
2060                0
2061            } else if dat.ppre.is_some() || (dat.flags & 0x0001/*CMF_FILE*/) == 0 {
2062                1
2063            } else {
2064                2
2065            };
2066            let mut lc_out: Option<Box<Cline>> = None;
2067            let mut isexact_out = 0i32;
2068            // c:2535 — comp_match(lpre, lsuf, s, cp, &lc, qu, &bpl, bcp,
2069            //          &bsl, bcs, &isexact).
2070            match crate::ported::zle::compmatch::comp_match(
2071                &lpre,
2072                &lsuf,
2073                word,
2074                None,
2075                Some(&mut lc_out),
2076                qu,
2077                None,
2078                0,
2079                None,
2080                0,
2081                &mut isexact_out,
2082            ) {
2083                Some(matched) => {
2084                    ms = matched;
2085                    _lc = lc_out;
2086                    isexact = isexact_out;
2087                }
2088                None => {
2089                    continue 'cand; // c:2541-2545 reject
2090                }
2091            }
2092        }
2093
2094        if doadd {
2095            // c:2547
2096            // c:2556 — add_match_data.
2097            let cm = add_match_data(
2098                0,
2099                &ms,
2100                word,
2101                _lc.clone(), // line — real Cline from comp_match
2102                dat.ipre.as_deref().unwrap_or(""),
2103                "", // ripre
2104                dat.isuf.as_deref().unwrap_or(""),
2105                dat.pre.as_deref().unwrap_or(""),
2106                dat.prpre.as_deref().unwrap_or(""),
2107                dat.ppre.as_deref().unwrap_or(""),
2108                None, // pline (path-prefix Cline; unused on this path)
2109                dat.psuf.as_deref().unwrap_or(""),
2110                None, // sline (path-suffix Cline; unused on this path)
2111                dat.suf.as_deref().unwrap_or(""),
2112                dat.flags,
2113                isexact,
2114            );
2115            let _ = cur_disp;
2116            let _ = cm;
2117            added += 1;
2118        } else {
2119            // c:2566
2120            if dat.apar.is_some() {
2121                // c:2567
2122                apar_list.push(ms.clone());
2123            }
2124            if dat.opar.is_some() {
2125                // c:2569
2126                opar_list.push(word.clone());
2127            }
2128        }
2129    }
2130
2131    // c:2602-2608 — apar/opar/dpar writeback.
2132    if let Some(ref name) = dat.apar {
2133        setaparam(name, apar_list);
2134    }
2135    if let Some(ref name) = dat.opar {
2136        setaparam(name, opar_list);
2137    }
2138
2139    // c:2610 — explanation emit.
2140    if dat.exp.is_some() {
2141        // c:2610
2142        addexpl(false);
2143    }
2144
2145    // c:2612-2614 — `<all>` placeholder when CAF_ALL set.
2146    let hasall = hasallmatch.load(Ordering::Relaxed);
2147    if hasall == 0 && (dat.aflags & CAF_ALL) != 0 {
2148        addmatch(
2149            "<all>",
2150            dat.flags | crate::ported::zle::comp_h::CMF_ALL,
2151            None,
2152            true,
2153        );
2154        hasallmatch.store(1, Ordering::Relaxed);
2155    }
2156
2157    // c:2616-2617 — dummy entries.
2158    while dat.dummies > 0 {
2159        addmatch(
2160            "",
2161            dat.flags | crate::ported::zle::comp_h::CMF_DUMMY,
2162            None,
2163            false,
2164        );
2165        dat.dummies -= 1;
2166    }
2167
2168    let _ = (ppl, psl, compignored_local, added);
2169    0 // c:2636
2170}
2171
2172// =====================================================================
2173// add_match_data — `Src/Zle/compcore.c:2643`.
2174// =====================================================================
2175
2176/// Direct port of `Cmatch add_match_data(int alt, char *str, char *orig,
2177///    Cline line, char *ipre, char *ripre, char *isuf, char *pre,
2178///    char *prpre, char *ppre, Cline pline, char *psuf, Cline sline,
2179///    char *suf, int flags, int exact)` from compcore.c:2643.
2180///
2181/// Builds one `Cmatch` from the supplied prefix/suffix bits plus the
2182/// surrounding Cline chain. Threads `line`/`pline`/`sline` through
2183/// `cline_matched` so the CLF_MATCHED state-machine update fires the
2184/// same way as C, then performs path-prefix/suffix splicing via
2185/// `bld_parts` to extend the Cline chain at the appropriate anchor.
2186#[allow(clippy::too_many_arguments)]
2187pub fn add_match_data(
2188    // c:2643
2189    alt: i32,
2190    str: &str,
2191    orig: &str,
2192    mut line: Option<Box<Cline>>,
2193    ipre_: &str,
2194    ripre_: &str,
2195    isuf_: &str,
2196    pre: &str,
2197    prpre: &str,
2198    ppre: &str,
2199    mut pline: Option<Box<Cline>>,
2200    psuf: &str,
2201    mut sline: Option<Box<Cline>>,
2202    suf: &str,
2203    flags: i32,
2204    exact: i32,
2205) -> Cmatch {
2206    // c:2663 — DPUTS(!line, "BUG: add_match_data() without cline")
2207    DPUTS!(line.is_none(), "BUG: add_match_data() without cline"); // c:2663
2208                                                                   // c:2657 — pick the active aminfo by `alt` (alternative path = fignore).
2209    let _ai_ref = if alt != 0 { &fainfo } else { &ainfo }; // c:2657
2210                                                           // c:2666-2671 — cline_matched(line); pline; sline.
2211    cline_matched(&mut line);
2212    if pline.is_some() {
2213        cline_matched(&mut pline);
2214    }
2215    if sline.is_some() {
2216        cline_matched(&mut sline);
2217    }
2218
2219    // c:2675-2697 — accumulator lengths.
2220    let psl = psuf.len();
2221    let isl = isuf_.len();
2222    let qisuf_v = qisuf_get(); // c:2680
2223    let qisl = qisuf_v.len();
2224    let _salen = (if sline.is_none() { psl } else { 0 }) + isl + qisl; // c:2675-2683
2225
2226    let ipl = ipre_.len();
2227    let _ppl = ppre.len();
2228    let _pl = pre.len();
2229    let qipre_v = qipre_get(); // c:2686
2230    let qipl_v = qipre_v.clone();
2231    let _qipl = qipl_v.len();
2232
2233    let _stl = str.len();
2234    let _lpl = ripre_.len();
2235    let _lsl = suf.len();
2236    let _ml = ipl;
2237
2238    // c:2671-2762 — path-suffix Cline splicing. salen accumulates psl
2239    // (psuf when no sline), isl (isuf), qisl (qisuf). When salen > 0
2240    // and line is non-empty, we walk to the tail and append the
2241    // bld_parts-built Cline for each contributing string.
2242    let psl_local = if sline.is_none() && !psuf.is_empty() {
2243        psuf.len() as i32
2244    } else {
2245        0
2246    };
2247    let isl_local = isuf_.len() as i32;
2248    let qisl_local = qisuf_v.len() as i32;
2249    let salen = psl_local + isl_local + qisl_local;
2250    if salen > 0 && line.is_some() {
2251        // Walk to the tail of line via .next.
2252        unsafe {
2253            let mut tail: *mut Option<Box<Cline>> = &mut line;
2254            while let Some(ref n) = *tail {
2255                if n.next.is_none() {
2256                    break;
2257                }
2258                tail = &mut (*tail).as_mut().unwrap().next;
2259            }
2260            // For each contributing string, build a Cline chain via
2261            // bld_parts and attach to the tail node's .next.
2262            if psl_local > 0 {
2263                let s = bld_parts(psuf, psl_local, psl_local, None, None);
2264                if let Some(node) = (*tail).as_mut() {
2265                    node.next = s;
2266                    while let Some(ref nn) = node.next {
2267                        if nn.next.is_none() {
2268                            break;
2269                        }
2270                        // already linked correctly; loop to advance tail
2271                        break;
2272                    }
2273                }
2274                // Walk to the new tail.
2275                while let Some(ref n) = *tail {
2276                    if n.next.is_none() {
2277                        break;
2278                    }
2279                    tail = &mut (*tail).as_mut().unwrap().next;
2280                }
2281            }
2282            if isl_local > 0 {
2283                let s = bld_parts(isuf_, isl_local, isl_local, None, None);
2284                if let Some(node) = (*tail).as_mut() {
2285                    node.next = s;
2286                }
2287                while let Some(ref n) = *tail {
2288                    if n.next.is_none() {
2289                        break;
2290                    }
2291                    tail = &mut (*tail).as_mut().unwrap().next;
2292                }
2293            }
2294            if qisl_local > 0 {
2295                let mut s = bld_parts(&qisuf_v, qisl_local, qisl_local, None, None);
2296                // c:2741 — qsl->flags |= CLF_SUF; qsl->suffix = qsl->prefix.
2297                if let Some(qsl) = s.as_mut() {
2298                    qsl.flags |= crate::ported::zle::comp_h::CLF_SUF;
2299                    qsl.suffix = qsl.prefix.take();
2300                }
2301                if let Some(node) = (*tail).as_mut() {
2302                    node.next = s;
2303                }
2304            }
2305        }
2306    }
2307
2308    // c:2766-2873 — path-prefix Cline splicing. palen accumulates qipl,
2309    // ipl, pl, ppl (when no pline). Each contributing string gets a
2310    // bld_parts Cline prepended to `line`.
2311    let qipl_local = qipre_v.len() as i32;
2312    let ipl_local = ipre_.len() as i32;
2313    let pl_local = pre.len() as i32;
2314    let ppl_local = if pline.is_none() && !ppre.is_empty() {
2315        ppre.len() as i32
2316    } else {
2317        0
2318    };
2319    if pl_local > 0 {
2320        if ppl_local > 0 {
2321            let p = bld_parts(ppre, ppl_local, ppl_local, None, None);
2322            // Walk p to its tail, link its tail's next to line.
2323            if p.is_some() {
2324                let mut p_chain = p;
2325                let mut tail: *mut Option<Box<Cline>> = &mut p_chain;
2326                unsafe {
2327                    while let Some(ref n) = *tail {
2328                        if n.next.is_none() {
2329                            break;
2330                        }
2331                        tail = &mut (*tail).as_mut().unwrap().next;
2332                    }
2333                    if let Some(t) = (*tail).as_mut() {
2334                        t.next = line.take();
2335                    }
2336                }
2337                line = p_chain;
2338            }
2339        }
2340        let p = bld_parts(pre, pl_local, pl_local, None, None);
2341        if let Some(mut head) = p {
2342            let mut t: *mut Option<Box<Cline>> = &mut head.next;
2343            unsafe {
2344                while (*t).is_some() {
2345                    if (*t).as_deref().unwrap().next.is_none() {
2346                        break;
2347                    }
2348                    t = &mut (*t).as_mut().unwrap().next;
2349                }
2350                *t = line.take();
2351            }
2352            line = Some(head);
2353        }
2354        if ipl_local > 0 {
2355            let p = bld_parts(ipre_, ipl_local, ipl_local, None, None);
2356            if let Some(mut head) = p {
2357                let mut t: *mut Option<Box<Cline>> = &mut head.next;
2358                unsafe {
2359                    while (*t).is_some() {
2360                        if (*t).as_deref().unwrap().next.is_none() {
2361                            break;
2362                        }
2363                        t = &mut (*t).as_mut().unwrap().next;
2364                    }
2365                    *t = line.take();
2366                }
2367                line = Some(head);
2368            }
2369        }
2370        if qipl_local > 0 {
2371            let p = bld_parts(&qipre_v, qipl_local, qipl_local, None, None);
2372            if let Some(mut head) = p {
2373                let mut t: *mut Option<Box<Cline>> = &mut head.next;
2374                unsafe {
2375                    while (*t).is_some() {
2376                        if (*t).as_deref().unwrap().next.is_none() {
2377                            break;
2378                        }
2379                        t = &mut (*t).as_mut().unwrap().next;
2380                    }
2381                    *t = line.take();
2382                }
2383                line = Some(head);
2384            }
2385        }
2386    } else if qipl_local + ipl_local + pl_local + ppl_local > 0 || pline.is_some() {
2387        // c:2827-2842 — consolidated apre buffer.
2388        let apre = format!(
2389            "{}{}{}{}",
2390            qipre_v.as_str(),
2391            ipre_,
2392            pre,
2393            if pline.is_none() { ppre } else { "" }
2394        );
2395        let apre_len = apre.len() as i32;
2396        if apre_len > 0 {
2397            let p = bld_parts(&apre, apre_len, apre_len, None, None);
2398            if let Some(mut head) = p {
2399                let mut t: *mut Option<Box<Cline>> = &mut head.next;
2400                unsafe {
2401                    while (*t).is_some() {
2402                        if (*t).as_deref().unwrap().next.is_none() {
2403                            break;
2404                        }
2405                        t = &mut (*t).as_mut().unwrap().next;
2406                    }
2407                    *t = line.take();
2408                }
2409                line = Some(head);
2410            }
2411        }
2412    }
2413
2414    let stl = str.len();
2415
2416    // c:2929-2932 — Cmatch allocation + str/orig/ppre/psuf.
2417    let mut cm = Cmatch::default(); // c:2929
2418    cm.str = Some(str.to_string()); // c:2930
2419    cm.orig = Some(orig.to_string()); // c:2931
2420    cm.ppre = if ppre.is_empty() {
2421        None
2422    } else {
2423        Some(ppre.into())
2424    }; // c:2932
2425    cm.psuf = if psuf.is_empty() {
2426        None
2427    } else {
2428        Some(psuf.into())
2429    }; // c:2933
2430
2431    // c:2934 — prpre only when CMF_FILE.
2432    cm.prpre = if (flags & CMF_FILE) != 0 && !prpre.is_empty() {
2433        Some(prpre.into())
2434    } else {
2435        None
2436    };
2437
2438    // c:2935-2938 — ipre = qipre + ipre (concat when qipre non-empty).
2439    // qipre_v already computed above.
2440    cm.ipre = if !qipre_v.is_empty() {
2441        if !ipre_.is_empty() {
2442            Some(format!("{}{}", qipre_v, ipre_))
2443        } else {
2444            Some(qipre_v.clone())
2445        }
2446    } else if !ipre_.is_empty() {
2447        Some(ipre_.into())
2448    } else {
2449        None
2450    };
2451
2452    cm.ripre = if ripre_.is_empty() {
2453        None
2454    } else {
2455        Some(ripre_.into())
2456    }; // c:2939
2457
2458    // c:2940-2943 — isuf = isuf + qisuf (concat when qisuf non-empty).
2459    cm.isuf = if !qisuf_v.is_empty() {
2460        if !isuf_.is_empty() {
2461            Some(format!("{}{}", isuf_, qisuf_v))
2462        } else {
2463            Some(qisuf_v.clone())
2464        }
2465    } else if !isuf_.is_empty() {
2466        Some(isuf_.into())
2467    } else {
2468        None
2469    };
2470
2471    cm.pre = if pre.is_empty() {
2472        None
2473    } else {
2474        Some(pre.into())
2475    }; // c:2944
2476    cm.suf = if suf.is_empty() {
2477        None
2478    } else {
2479        Some(suf.into())
2480    }; // c:2945
2481
2482    // c:2946 — flags + CMF_PACKED/CMF_ROWS from complist.
2483    let complist_s = COMPLIST
2484        .get()
2485        .and_then(|m| m.lock().ok().map(|g| g.clone()))
2486        .unwrap_or_default();
2487    let extra_flags = (if complist_s.contains("packed") {
2488        CMF_PACKED
2489    } else {
2490        0
2491    }) | (if complist_s.contains("rows") {
2492        CMF_ROWS
2493    } else {
2494        0
2495    });
2496    cm.flags = flags | extra_flags;
2497
2498    // c:2950-2951 — mode/fmode init to 0.
2499    cm.mode = 0;
2500    cm.fmode = 0;
2501    cm.modec = '\0';
2502    cm.fmodec = '\0';
2503
2504    // c:2952-2970 — CMF_FILE: stat the path for mode + modec.
2505    use crate::ported::zle::comp_h::CMF_FILE;
2506    if (flags & CMF_FILE) != 0 && !orig.is_empty() && !orig.ends_with('/') {
2507        let pb = format!("{}{}", cm.prpre.as_deref().unwrap_or("./"), orig);
2508        // c:2960 — ztat follow-symlink for mode.
2509        if let Some(meta) = ztat(&pb, false) {
2510            use std::os::unix::fs::MetadataExt;
2511            cm.mode = meta.mode();
2512        }
2513        // c:2965 — ztat without symlink-follow for fmode.
2514        if let Some(meta) = ztat(&pb, true) {
2515            use std::os::unix::fs::MetadataExt;
2516            cm.fmode = meta.mode();
2517        }
2518    }
2519
2520    // c:2974-2993 — brpl/brsl brace position arrays. Walk BRBEG/BREND
2521    // (the global Brinfo chains from `Src/Zle/zle_tricky.c`), reading
2522    // `qpos` for each entry to derive the position offset within the
2523    // match string. With no brace chain populated (zero-brace common
2524    // case) brpl/brsl stay empty.
2525    cm.brpl = BRBEG
2526        .get_or_init(|| Mutex::new(None))
2527        .lock()
2528        .ok()
2529        .and_then(|g| {
2530            g.as_ref().map(|head| {
2531                let mut out: Vec<i32> = Vec::new();
2532                let mut cur = Some(head.as_ref());
2533                while let Some(n) = cur {
2534                    out.push(n.qpos);
2535                    cur = n.next.as_deref();
2536                }
2537                out
2538            })
2539        })
2540        .unwrap_or_default();
2541    cm.brsl = BREND
2542        .get_or_init(|| Mutex::new(None))
2543        .lock()
2544        .ok()
2545        .and_then(|g| {
2546            g.as_ref().map(|head| {
2547                let mut out: Vec<i32> = Vec::new();
2548                let mut cur = Some(head.as_ref());
2549                while let Some(n) = cur {
2550                    out.push(n.qpos);
2551                    cur = n.next.as_deref();
2552                }
2553                out
2554            })
2555        })
2556        .unwrap_or_default();
2557
2558    cm.qipl = qipre_v.len() as i32; // c:2994
2559    cm.qisl = qisuf_v.len() as i32; // c:2995
2560                                    // c:2996 — autoq read.
2561    let autoq_v = AUTOQ
2562        .get()
2563        .and_then(|m| m.lock().ok().map(|g| g.clone()))
2564        .unwrap_or_default();
2565    cm.autoq = if !autoq_v.is_empty() {
2566        Some(autoq_v)
2567    } else if INBACKT.load(Ordering::Relaxed) != 0 {
2568        Some("`".into())
2569    } else {
2570        None
2571    };
2572
2573    cm.rems = None;
2574    cm.remf = None;
2575    cm.disp = None; // c:2997
2576
2577    // c:3003 — ai->line = join_clines(ai->line, line).
2578    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2579        if let Some(a) = g.as_mut() {
2580            let old_line = a.line.take();
2581            a.line = crate::ported::zle::compmatch::join_clines(old_line, line);
2582        }
2583    }
2584
2585    // c:3005 — mnum++.
2586    mnum.fetch_add(1, Ordering::Relaxed);
2587
2588    // c:3006 — ai->count++.
2589    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2590        if let Some(a) = g.as_mut() {
2591            a.count += 1;
2592        }
2593    }
2594
2595    // c:3008 — addlinknode((alt ? fmatches : matches), cm). Already
2596    // wired below via matches Vec push.
2597
2598    // c:3010-3011 — newmatches = 1; mgroup->new = 1.
2599    newmatches.store(1, Ordering::Relaxed);
2600
2601    // c:3012-3013 — compignored++ when alt.
2602    if alt != 0 {
2603        crate::ported::zle::complete::COMPIGNORED.fetch_add(1, Ordering::Relaxed);
2604    }
2605
2606    // c:3015-3016 — dolastprompt reset when complastprompt empty.
2607    let complastprompt_v = crate::ported::zle::complete::COMPLASTPREFIX
2608        .get()
2609        .and_then(|m| m.lock().ok().map(|g| g.clone()))
2610        .unwrap_or_default();
2611    if complastprompt_v.is_empty() {
2612        dolastprompt.store(0, Ordering::Relaxed);
2613    }
2614
2615    // c:3018-3023 — curexpl.count/fcount increment.
2616    if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2617        if let Some(e) = g.as_mut() {
2618            if alt != 0 {
2619                e.fcount += 1;
2620            } else {
2621                e.count += 1;
2622            }
2623        }
2624    }
2625
2626    // c:3024-3025 — ai->firstm = cm.
2627    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2628        if let Some(a) = g.as_mut() {
2629            if a.firstm.is_none() {
2630                a.firstm = Some(Box::new(cm.clone()));
2631            }
2632        }
2633    }
2634
2635    // c:3027-3034 — minmlen/maxmlen tracking.
2636    let lpl = cm.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
2637    let lsl = cm.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
2638    let ml = (stl + lpl + lsl) as i32;
2639    let cur_min = minmlen.load(Ordering::Relaxed);
2640    let cur_max = maxmlen.load(Ordering::Relaxed);
2641    if ml < cur_min {
2642        minmlen.store(ml, Ordering::Relaxed);
2643    }
2644    if ml > cur_max {
2645        maxmlen.store(ml, Ordering::Relaxed);
2646    }
2647
2648    // c:3037-3064 — exact-match tracking on ai.
2649    if exact != 0 {
2650        // c:3037
2651        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2652            if let Some(a) = g.as_mut() {
2653                if a.exact == 0 {
2654                    // c:3038
2655                    a.exact = useexact.load(Ordering::Relaxed);
2656                    a.exactm = Some(Box::new(cm.clone())); // c:3058
2657                } else if useexact.load(Ordering::Relaxed) != 0 {
2658                    // c:3059
2659                    // c:3060-3061 — ambiguous exact: set to 2, clear exactm.
2660                    a.exact = 2;
2661                    a.exactm = None;
2662                }
2663            }
2664        }
2665    }
2666
2667    // c:3064 — push cm into matches/fmatches LinkList.
2668    let cell = if alt != 0 {
2669        fmatches.get_or_init(|| Mutex::new(Vec::new()))
2670    } else {
2671        matches.get_or_init(|| Mutex::new(Vec::new()))
2672    };
2673    if let Ok(mut g) = cell.lock() {
2674        g.push(cm.clone());
2675    }
2676
2677    cm // c:3067 return cm
2678}
2679
2680// `lookup_complist_flags` deleted — Rust-only 8-line helper. Inlined
2681// at the single call site in callcompfunc (c:2049-2051).
2682
2683// =====================================================================
2684// begcmgroup — `Src/Zle/compcore.c:3073`.
2685// =====================================================================
2686
2687/// Port of `mod_export void begcmgroup(char *n, int flags)` from
2688/// compcore.c:3073.
2689pub fn begcmgroup(n: Option<&str>, flags: i32) {
2690    // c:3073
2691    if let Some(name) = n {
2692        // c:3073
2693        let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON                    // c:3085
2694                 | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
2695        let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
2696        if let Ok(g) = cell.lock() {
2697            for grp in g.iter() {
2698                // c:3078
2699                if grp.name.as_deref() == Some(name)                         // c:3084-3087
2700                    && (grp.flags & mask) == flags
2701                {
2702                    let active = grp.clone(); // c:3088
2703                    let mc = mgroup.get_or_init(|| Mutex::new(None));
2704                    if let Ok(mut s) = mc.lock() {
2705                        *s = Some(active);
2706                    }
2707                    return; // c:3095
2708                }
2709            }
2710        }
2711    }
2712    let mut grp = Cmgroup::default(); // c:3101
2713    grp.name = n.map(String::from); // c:3105
2714    grp.flags = flags; // c:3108
2715    let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
2716    if let Ok(mut g) = cell.lock() {
2717        g.insert(0, grp.clone()); // c:3121-3124
2718    }
2719    let mc = mgroup.get_or_init(|| Mutex::new(None));
2720    if let Ok(mut s) = mc.lock() {
2721        *s = Some(grp);
2722    }
2723    if let Ok(mut g) = expls.get_or_init(|| Mutex::new(Vec::new())).lock() {
2724        g.clear();
2725    }
2726    if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2727        g.clear();
2728    }
2729    if let Ok(mut g) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2730        g.clear();
2731    }
2732    if let Ok(mut g) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock() {
2733        g.clear();
2734    }
2735}
2736
2737// =====================================================================
2738// endcmgroup — `Src/Zle/compcore.c:3131`.
2739// =====================================================================
2740
2741/// Port of `mod_export void endcmgroup(char **ylist)` from
2742/// compcore.c:3131.
2743pub fn endcmgroup(ylist: Option<Vec<String>>) {
2744    // c:3131
2745    if let Ok(mut g) = mgroup.get_or_init(|| Mutex::new(None)).lock() {
2746        if let Some(grp) = g.as_mut() {
2747            grp.ylist = ylist.unwrap_or_default();
2748        } // c:3140
2749    }
2750}
2751
2752// =====================================================================
2753// addexpl — `Src/Zle/compcore.c:3140`.
2754// =====================================================================
2755
2756/// Port of `mod_export void addexpl(int always)` from compcore.c:3140.
2757pub fn addexpl(always: bool) {
2758    // c:3140
2759    let curexpl_snap = {
2760        let cell = curexpl.get_or_init(|| Mutex::new(None));
2761        cell.lock().ok().and_then(|g| g.clone())
2762    };
2763    let curexpl_str = match curexpl_snap.as_ref().and_then(|e| e.str.clone()) {
2764        Some(s) => s,
2765        None => return,
2766    };
2767    let curexpl_count = curexpl_snap.as_ref().map(|e| e.count).unwrap_or(0);
2768    let curexpl_fcount = curexpl_snap.as_ref().map(|e| e.fcount).unwrap_or(0);
2769
2770    let elist = expls.get_or_init(|| Mutex::new(Vec::new()));
2771    if let Ok(mut g) = elist.lock() {
2772        for e in g.iter_mut() {
2773            // c:3145
2774            if e.str.as_deref() == Some(curexpl_str.as_str()) {
2775                // c:3147
2776                e.count += curexpl_count; // c:3148
2777                e.fcount += curexpl_fcount; // c:3149
2778                if always {
2779                    // c:3150
2780                    e.always = 1;
2781                    nmessages.fetch_add(1, Ordering::Relaxed); // c:3152
2782                    newmatches.store(1, Ordering::Relaxed); // c:3153
2783                    let mc = mgroup.get_or_init(|| Mutex::new(None));
2784                    if let Ok(mut mg) = mc.lock() {
2785                        if let Some(grp) = mg.as_mut() {
2786                            grp.new_ = 1;
2787                        }
2788                    }
2789                }
2790                return; // c:3156
2791            }
2792        }
2793        if let Some(e) = curexpl_snap {
2794            // c:3159
2795            g.push(e);
2796        }
2797    }
2798    newmatches.store(1, Ordering::Relaxed); // c:3160
2799    if always {
2800        // c:3161
2801        let mc = mgroup.get_or_init(|| Mutex::new(None));
2802        if let Ok(mut mg) = mc.lock() {
2803            if let Some(grp) = mg.as_mut() {
2804                grp.new_ = 1;
2805            }
2806        }
2807        nmessages.fetch_add(1, Ordering::Relaxed); // c:3173
2808    }
2809}
2810
2811// =====================================================================
2812// matchcmp — `Src/Zle/compcore.c:3173`.
2813// =====================================================================
2814
2815/// Port of `static int matchcmp(Cmatch *a, Cmatch *b)` from
2816/// compcore.c:3173.
2817pub fn matchcmp(a: &Cmatch, b: &Cmatch) -> std::cmp::Ordering {
2818    // c:3173
2819    let order = MATCHORDER.load(Ordering::Relaxed);
2820    let sortdir = if (order & CGF_REVSORT) != 0 { -1 } else { 1 }; // c:3177
2821
2822    let cmp = (b.disp.is_some() as i32) - (a.disp.is_some() as i32); // c:3176
2823    let (as_, bs) = if (order & CGF_MATSORT) != 0 || (cmp == 0 && a.disp.is_none()) {
2824        (
2825            a.str.clone().unwrap_or_default(), // c:3181
2826            b.str.clone().unwrap_or_default(),
2827        ) // c:3182
2828    } else {
2829        if cmp != 0 {
2830            // c:3184
2831            let raw = (cmp as i32) * sortdir;
2832            return if raw < 0 {
2833                std::cmp::Ordering::Less
2834            }
2835            // c:3185
2836            else if raw > 0 {
2837                std::cmp::Ordering::Greater
2838            } else {
2839                std::cmp::Ordering::Equal
2840            };
2841        }
2842        let displine_cmp = (b.flags & CMF_DISPLINE) - (a.flags & CMF_DISPLINE); // c:3187
2843        if displine_cmp != 0 {
2844            // c:3188
2845            let raw = displine_cmp * sortdir;
2846            return if raw < 0 {
2847                std::cmp::Ordering::Less
2848            } else if raw > 0 {
2849                std::cmp::Ordering::Greater
2850            } else {
2851                std::cmp::Ordering::Equal
2852            };
2853        }
2854        (
2855            a.disp.clone().unwrap_or_default(), // c:3191
2856            b.disp.clone().unwrap_or_default(),
2857        ) // c:3192
2858    };
2859    let raw = sortdir
2860        * if as_ == bs {
2861            0
2862        } else if as_ < bs {
2863            -1
2864        } else {
2865            1
2866        };
2867    if raw < 0 {
2868        std::cmp::Ordering::Less
2869    }
2870    // c:3195
2871    else if raw > 0 {
2872        std::cmp::Ordering::Greater
2873    } else {
2874        std::cmp::Ordering::Equal
2875    }
2876}
2877
2878/// Port of `static int matcheq(Cmatch a, Cmatch b)` from
2879/// compcore.c:3206.
2880pub fn matcheq(a: &Cmatch, b: &Cmatch) -> bool {
2881    // c:3207
2882    matchstreq(a.ipre.as_ref(),  b.ipre.as_ref())  &&                        // c:3207
2883    matchstreq(a.pre.as_ref(),   b.pre.as_ref())   &&                        // c:3210
2884    matchstreq(a.ppre.as_ref(),  b.ppre.as_ref())  &&                        // c:3211
2885    matchstreq(a.psuf.as_ref(),  b.psuf.as_ref())  &&                        // c:3212
2886    matchstreq(a.suf.as_ref(),   b.suf.as_ref())   &&                        // c:3213
2887    matchstreq(a.str.as_ref(),  b.str.as_ref()) // c:3214
2888}
2889
2890// =====================================================================
2891// makearray — `Src/Zle/compcore.c:3224`.
2892// =====================================================================
2893
2894/// Port of `static Cmatch *makearray(LinkList l, int type, int flags,
2895///                                    int *np, int *nlp, int *llp)`
2896/// from compcore.c:3223. Returns `(arr, n, nl, ll)`.
2897///
2898/// `type` is fixed to `1` (match-sort path) for the in-file call sites
2899/// from `permmatches`. The `type=0` string-sort path on `lexpls` is
2900/// inlined at the `permmatches` call site (C uses a `(char **)` cast
2901/// trick that has no safe Rust equivalent).
2902pub fn makearray(mut rp: Vec<Cmatch>, flags: i32) -> (Vec<Cmatch>, i32, i32, i32) {
2903    // c:3224
2904    let mut n: i32 = rp.len() as i32; // c:3224
2905    let mut nl: i32 = 0; // c:3231
2906    let mut ll: i32 = 0; // c:3231
2907
2908    if n > 0 {
2909        // c:3258 (type==1 branch)
2910        if (flags & CGF_NOSORT) == 0 {
2911            // c:3259
2912            // Now sort the array (it contains matches).                     // c:3260
2913            MATCHORDER.store(flags, Ordering::Relaxed); // c:3261
2914            rp.sort_by(matchcmp); // c:3262 qsort matchcmp
2915
2916            if (flags & CGF_UNIQCON) == 0 {
2917                // c:3269 not -2
2918                // remove dupes
2919                let mut cp = 0usize; // c:3272
2920                let mut ap = 0usize;
2921                while ap < rp.len() {
2922                    // c:3274 for ap;*ap;ap++
2923                    if ap != cp {
2924                        rp.swap(ap, cp);
2925                    } // c:3275 *cp++ = *ap
2926                    cp += 1;
2927                    let mut bp = ap;
2928                    while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
2929                        bp += 1;
2930                        n -= 1; // c:3277 bp[1] && matcheq
2931                    }
2932                    let mut dup = 0i32; // c:3281
2933                    while bp + 1 < rp.len()
2934                        && rp[ap].disp.is_none()
2935                        && rp[bp + 1].disp.is_none()                         // c:3282 !disp
2936                        && rp[ap].str == rp[bp + 1].str
2937                    {
2938                        rp[bp + 1].flags |= CMF_MULT; // c:3284
2939                        dup = 1; // c:3285
2940                        bp += 1;
2941                    }
2942                    if dup != 0 {
2943                        // c:3287
2944                        rp[ap].flags |= CMF_FMULT; // c:3288
2945                    }
2946                    ap = bp + 1; // c:3279 ap = bp; ap++
2947                }
2948                rp.truncate(cp); // c:3291 *cp = NULL
2949            }
2950            for m in rp.iter() {
2951                // c:3293
2952                if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
2953                    // c:3294
2954                    ll += 1;
2955                }
2956                if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
2957                    // c:3296
2958                    nl += 1;
2959                }
2960            }
2961        } else {
2962            // c:3300 used -O nosort or -V
2963            if (flags & CGF_UNIQALL) == 0 && (flags & CGF_UNIQCON) == 0 {
2964                // c:3302 didn't use -1 or -2
2965                MATCHORDER.store(flags, Ordering::Relaxed); // c:3306
2966                let mut sp: Vec<Cmatch> = rp.clone(); // c:3309-3312 zhalloc + memcpy
2967                sp.sort_by(matchcmp); // c:3313 qsort matchcmp
2968
2969                let mut del = false; // c:3303
2970                                     // Sweep sorted dup-detection back onto rp via flag marks.
2971                for w in sp.windows(2) {
2972                    // c:3315-3329
2973                    if matcheq(&w[0], &w[1]) {
2974                        // Mark in original rp by str+disp equality.
2975                        for m in rp.iter_mut() {
2976                            if matcheq(m, &w[1]) {
2977                                m.flags = CMF_DELETE; // c:3318
2978                                del = true; // c:3319
2979                                break;
2980                            }
2981                        }
2982                    } else if w[0].disp.is_none() {
2983                        if w[1].disp.is_none() && w[0].str == w[1].str {
2984                            // c:3322
2985                            for m in rp.iter_mut() {
2986                                if matcheq(m, &w[1]) {
2987                                    m.flags |= CMF_MULT; // c:3324
2988                                    break;
2989                                }
2990                            }
2991                            for m in rp.iter_mut() {
2992                                if matcheq(m, &w[0]) {
2993                                    m.flags |= CMF_FMULT; // c:3328
2994                                    break;
2995                                }
2996                            }
2997                        }
2998                    }
2999                }
3000                if del {
3001                    // c:3332
3002                    rp.retain(|m| (m.flags & CMF_DELETE) == 0); // c:3334-3340
3003                    n = rp.len() as i32;
3004                }
3005            } else if (flags & CGF_UNIQCON) == 0 {
3006                // c:3344 -1 not -2
3007                let mut cp = 0usize;
3008                let mut ap = 0usize;
3009                while ap < rp.len() {
3010                    // c:3346
3011                    if ap != cp {
3012                        rp.swap(ap, cp);
3013                    }
3014                    cp += 1;
3015                    let mut bp = ap;
3016                    while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
3017                        bp += 1;
3018                        n -= 1; // c:3348
3019                    }
3020                    let mut dup = 0i32;
3021                    while bp + 1 < rp.len()
3022                        && rp[ap].disp.is_none()
3023                        && rp[bp + 1].disp.is_none()
3024                        && rp[ap].str == rp[bp + 1].str
3025                    {
3026                        rp[bp + 1].flags |= CMF_MULT; // c:3352
3027                        dup = 1; // c:3353
3028                        bp += 1;
3029                    }
3030                    if dup != 0 {
3031                        rp[ap].flags |= CMF_FMULT; // c:3356
3032                    }
3033                    ap = bp + 1;
3034                }
3035                rp.truncate(cp); // c:3359
3036            }
3037            for m in rp.iter() {
3038                // c:3361
3039                if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
3040                    // c:3362
3041                    ll += 1;
3042                }
3043                if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
3044                    // c:3364
3045                    nl += 1;
3046                }
3047            }
3048        }
3049    }
3050    (rp, n, nl, ll) // c:3366-3373
3051}
3052
3053// =====================================================================
3054// dupmatch — `Src/Zle/compcore.c:3370`.
3055// =====================================================================
3056
3057/// Port of `static Cmatch dupmatch(Cmatch m, int nbeg, int nend)` from
3058/// compcore.c:3370. Deep-copies one match; brpl/brsl are truncated to
3059/// nbeg/nend per the C body's nbeg/nend-sized `zalloc` + element copy.
3060pub fn dupmatch(m: &Cmatch, nbeg: i32, nend: i32) -> Cmatch {
3061    // c:3370
3062    let mut r = Cmatch::default(); // c:3370-3374
3063    r.str = m.str.clone(); // c:3376 ztrdup
3064    r.orig = m.orig.clone(); // c:3377
3065    r.ipre = m.ipre.clone(); // c:3378
3066    r.ripre = m.ripre.clone(); // c:3379
3067    r.isuf = m.isuf.clone(); // c:3380
3068    r.ppre = m.ppre.clone(); // c:3381
3069    r.psuf = m.psuf.clone(); // c:3382
3070    r.prpre = m.prpre.clone(); // c:3383
3071    r.pre = m.pre.clone(); // c:3384
3072    r.suf = m.suf.clone(); // c:3385
3073    r.flags = m.flags; // c:3386
3074    if !m.brpl.is_empty() {
3075        // c:3387
3076        let take = (nbeg as usize).min(m.brpl.len()); // c:3390 zalloc(nbeg)
3077        r.brpl = m.brpl[..take].to_vec(); // c:3392 element-wise copy
3078    } else {
3079        r.brpl = Vec::new(); // c:3395 NULL
3080    }
3081    if !m.brsl.is_empty() {
3082        // c:3396
3083        let take = (nend as usize).min(m.brsl.len()); // c:3399
3084        r.brsl = m.brsl[..take].to_vec(); // c:3401
3085    } else {
3086        r.brsl = Vec::new(); // c:3404
3087    }
3088    r.rems = m.rems.clone(); // c:3405
3089    r.remf = m.remf.clone(); // c:3406
3090    r.autoq = m.autoq.clone(); // c:3407
3091    r.qipl = m.qipl; // c:3408
3092    r.qisl = m.qisl; // c:3409
3093    r.disp = m.disp.clone(); // c:3410
3094    r.mode = m.mode; // c:3411
3095    r.modec = m.modec; // c:3412
3096    r.fmode = m.fmode; // c:3413
3097    r.fmodec = m.fmodec; // c:3414
3098    r // c:3416
3099}
3100
3101/// Port of `mod_export int permmatches(int last)` from compcore.c:3422.
3102/// Promotes the per-round `amatches` accumulator into the permanent
3103/// `pmatches` snapshot via deep-copy through `dupmatch`/`makearray`.
3104pub fn permmatches(last: i32) -> i32 {
3105    // c:3423
3106    let ofi = PERMMATCHES_FI.load(Ordering::Relaxed); // c:3423 ofi = fi
3107
3108    // c:3433 — `if (pmatches && !newmatches)`
3109    let pmatches_set = pmatches
3110        .get_or_init(|| Mutex::new(Vec::new()))
3111        .lock()
3112        .map(|g| !g.is_empty())
3113        .unwrap_or(false);
3114    if pmatches_set && newmatches.load(Ordering::Relaxed) == 0 {
3115        // c:3433
3116        if last != 0 && PERMMATCHES_FI.load(Ordering::Relaxed) != 0 {
3117            // c:3434
3118            // ainfo = fainfo                                                // c:3435
3119            let famref = fainfo
3120                .get_or_init(|| Mutex::new(None))
3121                .lock()
3122                .ok()
3123                .and_then(|g| g.clone());
3124            if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3125                *a = famref;
3126            }
3127        }
3128        return PERMMATCHES_FI.load(Ordering::Relaxed); // c:3437
3129    }
3130    newmatches.store(0, Ordering::Relaxed); // c:3439
3131    PERMMATCHES_FI.store(0, Ordering::Relaxed); // c:3439 fi = 0
3132
3133    {
3134        // pmatches = lmatches = NULL                                        // c:3441
3135        if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3136            g.clear();
3137        }
3138        if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
3139            *g = None;
3140        }
3141    }
3142    nmatches.store(0, Ordering::Relaxed); // c:3442
3143    smatches.store(0, Ordering::Relaxed); // c:3442
3144    diffmatches.store(0, Ordering::Relaxed); // c:3442
3145
3146    // c:3444 — `if (!ainfo->count)`.
3147    let ainfo_count = ainfo
3148        .get_or_init(|| Mutex::new(None))
3149        .lock()
3150        .ok()
3151        .and_then(|g| g.as_ref().map(|a| a.count))
3152        .unwrap_or(0);
3153    if ainfo_count == 0 {
3154        // c:3444
3155        if last != 0 {
3156            // c:3445
3157            let famref = fainfo
3158                .get_or_init(|| Mutex::new(None))
3159                .lock()
3160                .ok()
3161                .and_then(|g| g.clone());
3162            if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3163                *a = famref;
3164            }
3165        }
3166        PERMMATCHES_FI.store(1, Ordering::Relaxed); // c:3447
3167    }
3168
3169    let nbeg = NBRBEG.load(Ordering::Relaxed);
3170    let nend = NBREND.load(Ordering::Relaxed);
3171
3172    let mut gn: i32 = 1; // c:3429 gn = 1
3173    let mut mn: i32 = 1; // c:3429 mn = 1
3174    let fi = PERMMATCHES_FI.load(Ordering::Relaxed);
3175
3176    let groups_snapshot: Vec<Cmgroup> = {
3177        amatches
3178            .get_or_init(|| Mutex::new(Vec::new()))
3179            .lock()
3180            .ok()
3181            .map(|g| g.clone())
3182            .unwrap_or_default()
3183    };
3184    let mut new_pmatches: Vec<Cmgroup> = Vec::with_capacity(groups_snapshot.len());
3185
3186    for g_orig in groups_snapshot.into_iter() {
3187        // c:3449 while (g)
3188        let mut g = g_orig; // borrow-mut snapshot
3189        let must_rebuild = fi != ofi || g.perm.is_none() || g.new_ != 0; // c:3456
3190        if must_rebuild {
3191            // c:3456
3192            let src_list = if fi != 0 {
3193                g.lfmatches.clone()
3194            }
3195            // c:3457
3196            else {
3197                g.lmatches.clone()
3198            }; // c:3461
3199
3200            let (arr, nn, nl, ll) = makearray(src_list, g.flags); // c:3463
3201            g.mcount = nn; // c:3464
3202            g.lcount = nn - nl; // c:3465
3203            if g.lcount < 0 {
3204                g.lcount = 0;
3205            } // c:3466
3206            g.llcount = ll; // c:3467
3207            if !g.ylist.is_empty() {
3208                // c:3468
3209                g.lcount = g.ylist.len() as i32; // c:3469
3210                smatches.store(2, Ordering::Relaxed); // c:3470
3211            }
3212            // c:3472 — makearray(lexpls, 0, 0, &ecount, NULL, NULL).
3213            let mut exps = g.lexpls.clone(); // type=0 path
3214            g.ecount = exps.len() as i32;
3215            // c:3475 ccount = 0
3216            g.ccount = 0; // c:3475
3217            nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3477
3218            smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3478
3219            if g.mcount > 1 {
3220                // c:3480
3221                diffmatches.store(1, Ordering::Relaxed); // c:3481
3222            }
3223
3224            // n = (Cmgroup) zshcalloc(...)                                  // c:3483
3225            let mut n_grp = Cmgroup::default();
3226            // c:3487 — `if (g->perm) freematches(g->perm, 0)`. Drop on
3227            // perm Box<Cmgroup> reclaims the C `free` path.
3228            g.perm = None; // c:3490 g->perm = n
3229                           // Then below we set g.perm = Some(Box::new(n_grp.clone())).
3230
3231            n_grp.num = gn;
3232            gn += 1; // c:3499
3233            n_grp.flags = g.flags; // c:3500
3234            n_grp.mcount = g.mcount; // c:3501
3235            n_grp.matches = arr
3236                .iter() // c:3502-3505 dupmatch loop
3237                .map(|m| dupmatch(m, nbeg, nend))
3238                .collect();
3239            n_grp.name = g.name.clone(); // c:3504
3240            n_grp.lcount = g.lcount; // c:3508
3241            n_grp.llcount = g.llcount; // c:3509
3242            if !g.ylist.is_empty() {
3243                // c:3510
3244                n_grp.ylist = g.ylist.clone(); // c:3511 zarrdup
3245            } else {
3246                n_grp.ylist = Vec::new(); // c:3513
3247            }
3248            if g.ecount != 0 {
3249                // c:3515
3250                // Build n->expls from g->expls deep-copying str + (fi
3251                // ? fcount : count); always carries over; fcount = 0.
3252                n_grp.expls = exps
3253                    .drain(..)
3254                    .map(|o| Cexpl {
3255                        // c:3517-3525
3256                        count: if fi != 0 { o.fcount } else { o.count }, // c:3520
3257                        always: o.always,                                // c:3521
3258                        fcount: 0,                                       // c:3522
3259                        str: o.str.clone(),                              // c:3523 ztrdup
3260                    })
3261                    .collect();
3262                n_grp.ecount = g.ecount;
3263            } else {
3264                n_grp.expls = Vec::new(); // c:3528
3265            }
3266            n_grp.widths = Vec::new(); // c:3531
3267                                       // Stitch perm chain (prev/next handled implicitly by Vec).
3268            g.matches = arr; // mirror C: g->matches = makearray result
3269            g.perm = Some(Box::new(n_grp.clone())); // c:3490 g->perm = n
3270            new_pmatches.push(n_grp); // c:3492-3496
3271        } else {
3272            // reuse existing g->perm                                        // c:3534
3273            nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3540
3274            smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3541
3275            if g.mcount > 1 {
3276                diffmatches.store(1, Ordering::Relaxed); // c:3543
3277            }
3278            g.num = gn;
3279            gn += 1; // c:3546
3280            if let Some(p) = g.perm.as_deref() {
3281                new_pmatches.push(p.clone()); // c:3537 pmatches = g->perm
3282            }
3283        }
3284        g.new_ = 0; // c:3548
3285    }
3286
3287    // c:3551-3563 — assign rnum/gnum, recompute diffmatches/nbrbeg.
3288    let mut first_first: Option<Cmatch> = None;
3289    for g_pm in new_pmatches.iter_mut() {
3290        g_pm.nbrbeg = nbeg; // c:3552
3291        g_pm.nbrend = nend; // c:3553
3292        let mut rn = 1i32; // c:3554
3293        for m in g_pm.matches.iter_mut() {
3294            m.rnum = rn;
3295            rn += 1; // c:3555
3296            m.gnum = mn;
3297            mn += 1; // c:3556
3298        }
3299        if diffmatches.load(Ordering::Relaxed) == 0 && !g_pm.matches.is_empty() {
3300            match first_first.as_ref() {
3301                // c:3558
3302                Some(p0) => {
3303                    if !matcheq(&g_pm.matches[0], p0) {
3304                        diffmatches.store(1, Ordering::Relaxed); // c:3560
3305                    }
3306                }
3307                None => first_first = Some(g_pm.matches[0].clone()), // c:3562
3308            }
3309        }
3310    }
3311
3312    if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3313        *g = new_pmatches;
3314    }
3315
3316    hasperm.store(1, Ordering::Relaxed); // c:3565
3317    permmnum.store(mn - 1, Ordering::Relaxed); // c:3566
3318    permgnum.store(gn - 1, Ordering::Relaxed); // c:3567
3319    if let Ok(mut ld) = listdat
3320        .get_or_init(|| Mutex::new(Default::default()))
3321        .lock()
3322    {
3323        ld.valid = 0; // c:3568
3324    }
3325
3326    fi // c:3570
3327}
3328
3329// =====================================================================
3330// freematch / freematches — `Src/Zle/compcore.c:3575 / 3605`.
3331// =====================================================================
3332
3333/// Port of `static void freematch(Cmatch m, int nbeg, int nend)` from
3334/// `Src/Zle/compcore.c:3575`. C frees each Cmatch field via `zsfree`
3335/// (str/orig/ipre/ripre/isuf/ppre/psuf/pre/suf/prpre/rems/remf/disp/
3336/// autoq) and `zfree(m->brpl, nbeg * sizeof(int))` /
3337/// `zfree(m->brsl, nend * sizeof(int))` — all collapse to Rust's
3338/// automatic Drop on the owned String / `Vec<i32>` fields. nbeg/nend
3339/// kept on the signature for C parity (consumed by C as `zfree` size
3340/// args; Rust Vec carries its own length).
3341pub fn freematch(m: Cmatch, _nbeg: i32, _nend: i32) {
3342    // c:3577 — `if (!m) return;` — Rust's owned `m` is always valid;
3343    // dropping it on return runs every field's destructor (c:3579-3596
3344    // zsfree / zfree calls collapsed).
3345    drop(m); // c:3598 zfree(m)
3346}
3347
3348/// Direct port of `mod_export void freematches(Cmgroup g, int cm)` from
3349/// `Src/Zle/compcore.c:3605`. The C path walks the cmgroup chain freeing
3350/// each Cmatch + ylist + expls + widths + name; in Rust those are
3351/// owned by `Vec`/`Box`/`String` so Drop covers the per-node free.
3352/// The `cm` arm at c:3636-3637 (`minfo.cur = NULL`) is the only
3353/// side-effect that doesn't fall out of Rust's ownership model — wire
3354/// it explicitly.
3355pub fn freematches(g: Vec<Cmgroup>, cm: i32) {
3356    // c:3605
3357    drop(g);
3358    if cm != 0 {
3359        // c:3636
3360        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
3361            g.cur = None; // c:3637
3362        }
3363    }
3364}
3365
3366// =====================================================================
3367// Extern globals — declared in other C files, mirrored here per
3368// PORT.md Rule 9 ("stub the EXTERN dependencies ... locally with
3369// file:line citations to their home file") so the local body ports
3370// below have a value source. When the canonical Rust homes land,
3371// these become `pub use crate::ported::<canonical>::*` re-exports.
3372// =====================================================================
3373
3374/// Port of `mod_export int lastend` from `Src/Zle/compcore.c:276`.
3375/// Byte position in the metafied line where the most-recent
3376/// completion insertion ended.
3377pub static LASTEND: AtomicI32 = AtomicI32::new(0); // compcore.c:276
3378
3379/// Port of `mod_export int wb` from `Src/lex.c:120`. Word-begin
3380/// position in the metafied line for the currently-completing word.
3381pub static WB: AtomicI32 = AtomicI32::new(0); // lex.c:120
3382/// Port of `mod_export int we` from `Src/lex.c:120`. Word-end position.
3383pub static WE: AtomicI32 = AtomicI32::new(0); // lex.c:120
3384/// Port of `mod_export int zlemetacs` from `Src/lex.c:104`. Cursor
3385/// position in the metafied line.
3386pub static ZLEMETACS: AtomicI32 = AtomicI32::new(0); // lex.c:104
3387/// Port of `mod_export int zlemetall` from `Src/lex.c:104`. Length
3388/// of the metafied line.
3389pub static ZLEMETALL: AtomicI32 = AtomicI32::new(0); // lex.c:104
3390/// Port of `mod_export int addedx` from `Src/lex.c:115`. Non-zero
3391/// while a dummy `x` cursor marker is in the line being lexed
3392/// (so completion can capture the partial word at the cursor).
3393pub static ADDEDX: AtomicI32 = AtomicI32::new(0); // lex.c:115
3394
3395/// Port of `mod_export char *zlemetaline` from `Src/lex.c:103`. The
3396/// metafied edit buffer for the current ZLE session — `foredel`,
3397/// `inststr`, `selfinsert` operate on this directly when compcore's
3398/// error-recovery path fires (compcore.c:344-355).
3399pub static ZLEMETALINE: OnceLock<Mutex<String>> = OnceLock::new(); // lex.c:103
3400/// Port of `mod_export ZLE_STRING_T zleline` from `Src/zle_main.c`.
3401pub static ZLELINE: OnceLock<Mutex<String>> = OnceLock::new(); // zle_main.c
3402/// Port of `mod_export int zlecs` from `Src/zle_main.c`.
3403pub static ZLECS: AtomicI32 = AtomicI32::new(0); // zle_main.c
3404/// Port of `mod_export int zlell` from `Src/zle_main.c`.
3405pub static ZLELL: AtomicI32 = AtomicI32::new(0); // zle_main.c
3406/// Port of `mod_export int inwhat` from `Src/lex.c:110`. Lex context
3407/// classification — IN_NOTHING / IN_CMD / IN_COND / IN_MATH / IN_PAR /
3408/// IN_ENV.
3409pub static INWHAT: AtomicI32 = AtomicI32::new(0); // lex.c:110
3410/// Port of `mod_export int zmult` from `Src/zle_main.c`. Numeric
3411/// prefix multiplier for the current ZLE command.
3412pub static ZMULT: AtomicI32 = AtomicI32::new(1); // zle_main.c
3413/// Port of `mod_export char *compfunc` from `Src/Zle/zle_tricky.c:143`.
3414/// Name of the user completion shell function — non-empty when the
3415/// new completion system (`compsys`) is active; empty for compctl.
3416pub static compfunc: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // zle_tricky.c:143
3417/// Port of `mod_export char *comppatmatch` from `Src/Zle/zle_tricky.c`.
3418/// `$compstate[pattern_match]` — when non-empty + non-"\0" enables
3419/// pattern-aware matching for parameter-name completion.
3420pub static comppatmatch: OnceLock<Mutex<Option<String>>> = OnceLock::new();
3421/// Port of `mod_export char *compqstack` from `Src/Zle/compcore.c`.
3422/// Quoting-state stack (1 char per nesting level).
3423pub static compqstack: OnceLock<Mutex<String>> = OnceLock::new();
3424// =====================================================================
3425// File-scope globals — `Src/Zle/compcore.c:36-279`.
3426// =====================================================================
3427
3428/// Port of `int useexact` from compcore.c:36.
3429pub static useexact: AtomicI32 = AtomicI32::new(0); // c:36
3430/// Port of `int useline` from compcore.c:36.
3431pub static useline: AtomicI32 = AtomicI32::new(0); // c:36
3432/// Port of `int uselist` from compcore.c:36.
3433pub static uselist: AtomicI32 = AtomicI32::new(0); // c:36
3434/// Port of `int forcelist` from compcore.c:36.
3435pub static forcelist: AtomicI32 = AtomicI32::new(0); // c:36
3436/// Port of `int startauto` from compcore.c:36.
3437pub static startauto: AtomicI32 = AtomicI32::new(0); // c:36
3438
3439/// Port of `mod_export int iforcemenu` from compcore.c:39.
3440pub static iforcemenu: AtomicI32 = AtomicI32::new(0); // c:39
3441
3442/// Port of `mod_export int dolastprompt` from compcore.c:44.
3443pub static dolastprompt: AtomicI32 = AtomicI32::new(0); // c:44
3444
3445/// Port of `mod_export int oldlist` from compcore.c:49.
3446pub static oldlist: AtomicI32 = AtomicI32::new(0); // c:49
3447/// Port of `mod_export int oldins` from compcore.c:49.
3448pub static oldins: AtomicI32 = AtomicI32::new(0); // c:49
3449
3450/// Port of `int origlpre` from compcore.c:54.
3451pub static origlpre: AtomicI32 = AtomicI32::new(0); // c:54
3452/// Port of `int origlsuf` from compcore.c:54.
3453pub static origlsuf: AtomicI32 = AtomicI32::new(0); // c:54
3454/// Port of `int lenchanged` from compcore.c:54.
3455pub static lenchanged: AtomicI32 = AtomicI32::new(0); // c:54
3456
3457/// Port of `int movetoend` from compcore.c:61.
3458pub static movetoend: AtomicI32 = AtomicI32::new(0); // c:61
3459
3460/// Port of `mod_export int insmnum` from compcore.c:66.
3461pub static insmnum: AtomicI32 = AtomicI32::new(0); // c:66
3462/// Port of `mod_export int insspace` from compcore.c:66.
3463pub static insspace: AtomicI32 = AtomicI32::new(0); // c:66
3464
3465/// Port of `mod_export int menuacc` from compcore.c:81.
3466pub static menuacc: AtomicI32 = AtomicI32::new(0); // c:81
3467
3468/// Port of `int hasunqu` from compcore.c:86.
3469pub static hasunqu: AtomicI32 = AtomicI32::new(0); // c:86
3470/// Port of `int useqbr` from compcore.c:86.
3471pub static useqbr: AtomicI32 = AtomicI32::new(0); // c:86
3472/// Port of `int brpcs` from compcore.c:86.
3473pub static brpcs: AtomicI32 = AtomicI32::new(0); // c:86
3474/// Port of `int brscs` from compcore.c:86.
3475pub static brscs: AtomicI32 = AtomicI32::new(0); // c:86
3476
3477/// Port of `mod_export int ispar` from compcore.c:91.
3478pub static ispar: AtomicI32 = AtomicI32::new(0); // c:91
3479/// Port of `mod_export int linwhat` from compcore.c:91.
3480pub static linwhat: AtomicI32 = AtomicI32::new(0); // c:91
3481
3482/// Port of `char *parpre` from compcore.c:96.
3483pub static parpre: OnceLock<Mutex<String>> = OnceLock::new(); // c:96
3484
3485/// Port of `int parflags` from compcore.c:101.
3486pub static parflags: AtomicI32 = AtomicI32::new(0); // c:101
3487
3488/// Port of `mod_export int mflags` from compcore.c:106.
3489pub static mflags: AtomicI32 = AtomicI32::new(0); // c:106
3490
3491/// Port of `int parq` from compcore.c:111.
3492pub static parq: AtomicI32 = AtomicI32::new(0); // c:111
3493/// Port of `int eparq` from compcore.c:111.
3494pub static eparq: AtomicI32 = AtomicI32::new(0); // c:111
3495
3496/// Port of `mod_export char *ipre` from compcore.c:118.
3497pub static ipre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
3498/// Port of `mod_export char *ripre` from compcore.c:118.
3499pub static ripre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
3500/// Port of `mod_export char *isuf` from compcore.c:118.
3501pub static isuf: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
3502
3503/// Port of `mod_export LinkList matches` from compcore.c:124.
3504pub static matches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:124
3505/// Port of `LinkList fmatches` from compcore.c:126.
3506pub static fmatches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:126
3507
3508/// Port of `mod_export Cmgroup amatches` from compcore.c:135.
3509pub static amatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
3510/// Port of `mod_export Cmgroup pmatches` from compcore.c:135.
3511pub static pmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
3512/// Port of `mod_export Cmgroup lastmatches` from compcore.c:135.
3513pub static lastmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
3514/// Port of `mod_export Cmgroup lmatches` from compcore.c:135. Last
3515/// element pointer in the perm list; here a single-slot holder.
3516pub static lmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
3517/// Port of `mod_export Cmgroup lastlmatches` from compcore.c:135.
3518pub static lastlmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
3519
3520/// Port of `mod_export int hasoldlist` from compcore.c:140.
3521pub static hasoldlist: AtomicI32 = AtomicI32::new(0); // c:140
3522/// Port of `mod_export int hasperm` from compcore.c:140.
3523pub static hasperm: AtomicI32 = AtomicI32::new(0); // c:140
3524/// Port of `int hasallmatch` from compcore.c:145.
3525pub static hasallmatch: AtomicI32 = AtomicI32::new(0); // c:145
3526
3527/// Port of `mod_export int newmatches` from compcore.c:150.
3528pub static newmatches: AtomicI32 = AtomicI32::new(0); // c:150
3529
3530/// Port of `mod_export int permmnum` from compcore.c:155.
3531pub static permmnum: AtomicI32 = AtomicI32::new(0); // c:155
3532/// Port of `mod_export int permgnum` from compcore.c:155.
3533pub static permgnum: AtomicI32 = AtomicI32::new(0); // c:155
3534/// Port of `mod_export int lastpermmnum` from compcore.c:155.
3535pub static lastpermmnum: AtomicI32 = AtomicI32::new(0); // c:155
3536/// Port of `mod_export int lastpermgnum` from compcore.c:155.
3537pub static lastpermgnum: AtomicI32 = AtomicI32::new(0); // c:155
3538
3539/// Port of `mod_export int nmatches` from compcore.c:160.
3540pub static nmatches: AtomicI32 = AtomicI32::new(0); // c:160
3541/// Port of `mod_export int smatches` from compcore.c:162.
3542pub static smatches: AtomicI32 = AtomicI32::new(0); // c:162
3543
3544/// Port of `mod_export int diffmatches` from compcore.c:167.
3545pub static diffmatches: AtomicI32 = AtomicI32::new(0); // c:167
3546
3547/// Port of `mod_export int nmessages` from compcore.c:172.
3548pub static nmessages: AtomicI32 = AtomicI32::new(0); // c:172
3549
3550/// Port of `mod_export int onlyexpl` from compcore.c:177.
3551pub static onlyexpl: AtomicI32 = AtomicI32::new(0); // c:177
3552
3553/// Port of `mod_export struct cldata listdat` from compcore.c:182.
3554pub static listdat: OnceLock<Mutex<crate::ported::zle::comp_h::Cldata>> = OnceLock::new(); // c:182
3555
3556/// Port of `mod_export int ispattern` from compcore.c:187.
3557pub static ispattern: AtomicI32 = AtomicI32::new(0); // c:187
3558/// Port of `mod_export int haspattern` from compcore.c:187.
3559pub static haspattern: AtomicI32 = AtomicI32::new(0); // c:187
3560
3561/// Port of `mod_export int hasmatched` from compcore.c:192.
3562pub static hasmatched: AtomicI32 = AtomicI32::new(0); // c:192
3563/// Port of `mod_export int hasunmatched` from compcore.c:192.
3564pub static hasunmatched: AtomicI32 = AtomicI32::new(0); // c:192
3565
3566/// Port of `Cmgroup mgroup` from compcore.c:197.
3567pub static mgroup: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:197
3568
3569/// Port of `mod_export int mnum` from compcore.c:202.
3570pub static mnum: AtomicI32 = AtomicI32::new(0); // c:202
3571
3572/// Port of `mod_export int unambig_mnum` from compcore.c:207.
3573pub static unambig_mnum: AtomicI32 = AtomicI32::new(0); // c:207
3574
3575/// Port of `int maxmlen` from compcore.c:212.
3576pub static maxmlen: AtomicI32 = AtomicI32::new(0); // c:212
3577/// Port of `int minmlen` from compcore.c:212.
3578pub static minmlen: AtomicI32 = AtomicI32::new(0); // c:212
3579
3580/// Port of `LinkList expls` from compcore.c:218.
3581pub static expls: OnceLock<Mutex<Vec<Cexpl>>> = OnceLock::new(); // c:218
3582
3583/// Port of `mod_export Cexpl curexpl` from compcore.c:221.
3584pub static curexpl: OnceLock<Mutex<Option<Cexpl>>> = OnceLock::new(); // c:221
3585
3586/// Port of `LinkList matchers` from compcore.c:236. The C list holds
3587/// `Cmatcher` pointers (the parsed match-spec chains pushed by
3588/// addmatches's `add_bmatchers` block). Rust port mirrors that with
3589/// owned `Box<Cmatcher>` entries.
3590pub static matchers: OnceLock<Mutex<Vec<Box<crate::ported::zle::comp_h::Cmatcher>>>> =
3591    OnceLock::new(); // c:236
3592
3593/// Port of `mod_export Aminfo ainfo` from compcore.c:246.
3594pub static ainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
3595/// Port of `mod_export Aminfo fainfo` from compcore.c:246.
3596pub static fainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
3597
3598/// Port of `mod_export LinkList allccs` from compcore.c:259.
3599pub static allccs: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:259
3600
3601/// Port of `int fromcomp` from compcore.c:271.
3602pub static fromcomp: AtomicI32 = AtomicI32::new(0); // c:271
3603
3604/// Port of `mod_export int lastend` from compcore.c:276.
3605pub static lastend: AtomicI32 = AtomicI32::new(0); // c:276
3606
3607/// Port of `mod_export Brinfo brbeg` from `Src/Zle/zle_tricky.c`.
3608/// Linked list of opening-brace positions in the word being completed.
3609pub static BRBEG: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brbeg
3610
3611/// Port of `mod_export Brinfo brend` from `Src/Zle/zle_tricky.c`.
3612/// Linked list of closing-brace positions in the word being completed.
3613pub static BREND: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brend
3614
3615/// Port of `static int oldmenucmp` from compcore.c:457.
3616pub static OLDMENUCMP: AtomicI32 = AtomicI32::new(0); // c:457
3617
3618/// Port of `static int parwb` from compcore.c:540.
3619pub static PARWB: AtomicI32 = AtomicI32::new(0); // c:540
3620/// Port of `static int parwe` from compcore.c:540.
3621pub static PARWE: AtomicI32 = AtomicI32::new(0); // c:540
3622/// Port of `static int paroffs` from compcore.c:540.
3623pub static PAROFFS: AtomicI32 = AtomicI32::new(0); // c:540
3624
3625/// Port of `static int matchorder` from compcore.c:3169.
3626pub static MATCHORDER: AtomicI32 = AtomicI32::new(0); // c:3169
3627
3628/// Port of `mod_export int lastchar` from `Src/Zle/zle_main.c`. Last
3629/// keyboard char consumed by the binding loop — read by `selfinsert`.
3630pub static LASTCHAR: AtomicI32 = AtomicI32::new(0); // zle_main.c
3631                                                    // minfo_clear_cur / minfo_asked_zero deleted — Rust-only 2-line
3632                                                    // wrappers around C's inline writes `minfo.cur = NULL` and
3633                                                    // `minfo.asked = 0`. All call sites inlined.
3634
3635/// Direct port of `struct menuinfo minfo` — `Src/Zle/zle_tricky.c`
3636/// (the single file-scope instance). The struct type itself lives
3637/// in `comp_h.rs::Menuinfo` (port of comp.h:284-295).
3638pub static MINFO: OnceLock<Mutex<Menuinfo>> = OnceLock::new(); // zle_tricky.c minfo
3639
3640// =====================================================================
3641// matcheq — `Src/Zle/compcore.c:3203-3215`.
3642// =====================================================================
3643
3644#[inline]
3645fn matchstreq(a: Option<&String>, b: Option<&String>) -> bool {
3646    // c:3207
3647    match (a, b) {
3648        (None, None) => true,
3649        (Some(x), Some(y)) => x == y,
3650        _ => false,
3651    }
3652}
3653
3654/// First-match shortcut path from compcore.c:398-411. `Cmgroup m = amatches;
3655/// while (!m->mcount) m = m->next; do_single(m->matches[0])`.
3656fn do_single_first_match() {
3657    // c:398
3658    let groups = amatches
3659        .get_or_init(|| Mutex::new(Vec::new()))
3660        .lock()
3661        .ok()
3662        .map(|g| g.clone())
3663        .unwrap_or_default();
3664    let first = groups
3665        .into_iter()
3666        .find(|g| g.mcount > 0)
3667        .and_then(|g| g.matches.first().cloned());
3668    if let Some(m) = first {
3669        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
3670            g.cur = None;
3671        } // c:407
3672        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
3673            g.asked = 0;
3674        } // c:408
3675          // c:409 — `do_single(m)`. Inlined: drop the Cmatch payload onto
3676          // MINFO.cur so the listing path picks it up (matches the C
3677          // behavior of routing the single-match insert through minfo).
3678        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
3679            g.cur = Some(Box::new(m));
3680        }
3681    }
3682}
3683
3684/// compcore.c:444 `compend:` epilogue — free matchers, snap zlemetacs.
3685fn goto_compend(ret: i32) -> i32 {
3686    // c:444
3687    if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
3688        g.clear(); // c:445-446 freecmatcher loop
3689    }
3690    let line_len = ZLEMETALL.load(Ordering::Relaxed); // c:448 strlen(zlemetaline)
3691    if ZLEMETACS.load(Ordering::Relaxed) > line_len {
3692        // c:449
3693        ZLEMETACS.store(line_len, Ordering::Relaxed); // c:450
3694    }
3695    ret // c:453
3696}
3697
3698// `COMP_LIST_COMPLETE` / `QT_NONE_STUB` / `QT_BACKSLASH_STUB` local
3699// aliases deleted — call sites now reach the real C-side constants
3700// directly (`crate::ported::zle::zle_h::COMP_LIST_COMPLETE`,
3701// `QT_NONE`, `QT_BACKSLASH`).
3702// The local `COMP_LIST_COMPLETE = 2` was a value-mismatch bug (the
3703// real constant is 1 per `Src/Zle/zle.h:357`).
3704
3705// `char_from_qt` deleted — Rust-only 1-line `(qt as u8) as char`
3706// helper. Inlined at the two call sites in get_compstate_str.
3707
3708// `showinglist_stub` / `showinglist_set` / `clearlist_set` /
3709// `listshown_stub` / `instring_stub` deleted — Rust-only 1-line
3710// accessors for C globals (SHOWINGLIST / CLEARLIST / LISTSHOWN /
3711// INSTRING). C reads/writes the bare globals inline; callers in
3712// compcore.rs now do `<GLOBAL>.load(Ordering::Relaxed)` /
3713// `<GLOBAL>.store(v, Ordering::Relaxed)` directly.
3714// `fn foredel` / `fn inststr` locals deleted — both duplicated
3715// canonical ports living in their proper home files:
3716//   - foredel: `Src/Zle/zle_utils.c:1105`
3717//     → `foredel`
3718//   - inststr: macro `Src/Zle/zle_tricky.c:57` (inststr(X) →
3719//     inststrlen(X,1,-1)) → `inststr`
3720// The duplicates here narrowed C signatures (foredel dropped `flags`,
3721// inststr dropped the i32 return + duplicated inststrlen's body) and
3722// violated Rule C (every decl in its mirroring C file). Callers in
3723// this module now route through the canonical ported.
3724/// `IN_NOTHING_LW` constant.
3725pub const IN_NOTHING_LW: i32 = 0; // lex.h
3726/// `IN_CMD_LW` constant.
3727pub const IN_CMD_LW: i32 = 1; // lex.h
3728/// `IN_COND_LW` constant.
3729pub const IN_COND_LW: i32 = 2; // lex.h
3730/// `IN_MATH_LW` constant.
3731pub const IN_MATH_LW: i32 = 3; // lex.h
3732/// `IN_PAR_LW` constant.
3733pub const IN_PAR_LW: i32 = 4; // lex.h
3734/// `IN_ENV_LW` constant.
3735pub const IN_ENV_LW: i32 = 5; // lex.h
3736                              // `origline_stub` / `origcs_stub` deleted — Rust-only 1-line
3737                              // accessors for the `ORIGLINE` / `ORIGCS` globals (ports of C
3738                              // `origline` / `origcs` at zle_tricky.c:75 etc.). C reads these
3739                              // globals inline; callers in compcore.rs now do the lock/load
3740                              // directly.
3741/// Port of `void unmetafy_line(void)` from `zle_tricky.c:995`.
3742///
3743/// C body:
3744///   `zlemetaline[zlemetall] = '\0';
3745///    zleline = stringaszleline(zlemetaline, zlemetacs, &zlell,
3746///                              &linesz, &zlecs);
3747///    free(zlemetaline); zlemetaline = NULL;
3748///    CCRIGHT();`
3749///
3750/// Reads ZLEMETALINE, decodes via the canonical stringaszleline
3751/// (handles incs adjustment + unmetafy + UTF-8 decode), populates
3752/// ZLELINE / ZLELL / ZLECS, clears ZLEMETALINE/ZLEMETALL.
3753fn unmetafy_line() {
3754    // zle_tricky.c:995
3755    let meta = ZLEMETALINE
3756        .get_or_init(|| Mutex::new(String::new()))
3757        .lock()
3758        .map(|g| g.clone())
3759        .unwrap_or_default();
3760    let zlemetacs = ZLEMETACS.load(Ordering::Relaxed) as i32;
3761    let mut out_ll: i32 = 0;
3762    let mut out_cs: i32 = 0;
3763    // c:998 — `zleline = stringaszleline(zlemetaline, zlemetacs, &zlell, &linesz, &zlecs);`
3764    let line = crate::ported::zle::zle_utils::stringaszleline(
3765        &meta,
3766        zlemetacs,
3767        Some(&mut out_ll),
3768        None,
3769        Some(&mut out_cs),
3770    );
3771    if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
3772        *g = line.iter().collect();
3773    }
3774    ZLELL.store(out_ll, Ordering::Relaxed);
3775    ZLECS.store(out_cs, Ordering::Relaxed);
3776    // c:1001-1002 — `free(zlemetaline); zlemetaline = NULL;`. Rust:
3777    // clear the buffer + zero the length to mark meta-mode inactive.
3778    if let Some(m) = ZLEMETALINE.get() {
3779        if let Ok(mut g) = m.lock() {
3780            g.clear();
3781        }
3782    }
3783    ZLEMETALL.store(0, Ordering::Relaxed);
3784    // c:1007 — CCRIGHT(): combining-char alignment fixup. No-op in
3785    // this Rust port (handled by stringaszleline's codepoint walk).
3786}
3787
3788/// Port of `void metafy_line(void)` from `zle_tricky.c:978`.
3789///
3790/// C body:
3791///   `zlemetaline = zlelineasstring(zleline, zlell, zlecs,
3792///                                  &zlemetall, &zlemetacs, 0);
3793///    metalinesz = zlemetall;
3794///    free(zleline); zleline = NULL;`
3795///
3796/// Reads ZLELINE, encodes via the canonical zlelineasstring (handles
3797/// wcrtomb + metafy expansion), populates ZLEMETALINE / ZLEMETALL /
3798/// ZLEMETACS, clears ZLELINE/ZLELL.
3799fn metafy_line() {
3800    // zle_tricky.c:978
3801    let raw_vec: Vec<char> = ZLELINE
3802        .get_or_init(|| Mutex::new(String::new()))
3803        .lock()
3804        .map(|g| g.chars().collect())
3805        .unwrap_or_default();
3806    let zlell = raw_vec.len();
3807    let zlecs = ZLECS.load(Ordering::Relaxed);
3808    let mut out_ll: i32 = 0;
3809    let mut out_cs: i32 = 0;
3810    // c:982 — `zlemetaline = zlelineasstring(zleline, zlell, zlecs, &zlemetall, &zlemetacs, 0);`
3811    let meta = crate::ported::zle::zle_utils::zlelineasstring(
3812        &raw_vec,
3813        zlell,
3814        zlecs,
3815        Some(&mut out_ll),
3816        Some(&mut out_cs),
3817        0,
3818    );
3819    if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
3820        *g = meta;
3821    }
3822    ZLEMETALL.store(out_ll, Ordering::Relaxed);
3823    ZLEMETACS.store(out_cs, Ordering::Relaxed);
3824    // c:985 — `metalinesz = zlemetall;`. Rust String grows on demand;
3825    // no separate sizeline tracker.
3826    // c:989-990 — `free(zleline); zleline = NULL;`. Rust: clear the
3827    // buffer + zero ZLELL.
3828    if let Ok(mut g) = ZLELINE.get().unwrap().lock() {
3829        g.clear();
3830    }
3831    ZLELL.store(0, Ordering::Relaxed);
3832}
3833
3834fn opt_isset(name: &str) -> i32 {
3835    // options.c
3836    if crate::ported::options::opt_state_get(name).unwrap_or(false) {
3837        1
3838    } else {
3839        0
3840    }
3841}
3842/// Real call into `getiparam(name)` — the canonical paramtab read.
3843/// Mirrors C's `getiparam` at params.c:3044 which reads the global
3844/// `paramtab` directly via `gethashnode2`.
3845fn env_iparam(name: &str) -> i32 {
3846    // params.c:3044
3847    crate::ported::params::getiparam(name) as i32
3848}
3849fn lastprebr_set(s: &str) {
3850    // zle_tricky.c lastprebr
3851    if let Ok(mut g) = LASTPREBR.get_or_init(|| Mutex::new(String::new())).lock() {
3852        *g = s.to_string();
3853    }
3854}
3855fn lastpostbr_set(s: &str) {
3856    // zle_tricky.c lastpostbr
3857    if let Ok(mut g) = LASTPOSTBR.get_or_init(|| Mutex::new(String::new())).lock() {
3858        *g = s.to_string();
3859    }
3860}
3861
3862/// Choose `$compstate[context]` per the lex classification in `inwhat`
3863/// (and the `ispar` modifier). Direct lift of compcore.c:591-617.
3864fn compcontext_for(_s: &str) -> String {
3865    // c:591
3866    let ip = ispar.load(Ordering::Relaxed); // c:599
3867    if ip == 2 {
3868        return "brace_parameter".into();
3869    } // c:600
3870    if ip == 1 {
3871        return "parameter".into();
3872    } // c:601
3873    let lw = linwhat.load(Ordering::Relaxed); // c:602
3874    match lw {
3875        // c:602
3876        x if x == IN_PAR_LW => "assign_parameter".into(), // c:603
3877        x if x == IN_MATH_LW => "math".into(),            // c:604-611
3878        x if x == IN_COND_LW => "condition".into(),       // c:613
3879        x if x == IN_ENV_LW => "value".into(),            // c:615
3880        _ => "command".into(),                            // c:617
3881    }
3882}
3883
3884/// File-scope `int offs` from `Src/Zle/zle_tricky.c:88`. The C source
3885/// declares this as `mod_export`; mirrored here per Rule 9 since it's
3886/// not yet at a canonical Rust home.
3887pub static OFFS: AtomicI32 = AtomicI32::new(0); // zle_tricky.c:88
3888
3889/// File-scope `Compctl freecl` from `Src/Zle/compcore.c:255`. The
3890/// freelist of available Compctl slots for the current completion call.
3891pub static freecl: OnceLock<Mutex<Option<i32>>> = OnceLock::new(); // c:255
3892
3893/// Real call into `doshfunc` — `Src/exec.c`. Looks up the function
3894/// in the global shfunctab (`getshfunc`) and dispatches via the VM's
3895/// `functions_compiled` map. Returns the function's exit status
3896/// (LASTVAL after the call), matching C's `doshfunc` return value.
3897pub fn shfunc_call(name: &str) -> i32 {
3898    // exec.c
3899    if crate::ported::utils::getshfunc(name).is_none() {
3900        // c:exec.c:5800
3901        return 1; // missing fn → status 1
3902    }
3903    // Route through the canonical exec accessors dispatcher so the
3904    // function actually executes. Hook returns Option<i32>; None
3905    // means no executor context is set up yet (fall back to
3906    // LASTVAL read).
3907    crate::ported::exec::dispatch_function_call(name, &[])
3908        .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
3909}
3910/// Real call into `setsparam(&format!("compstate[{key}]"), val)` — the
3911/// canonical paramtab write. Mirrors C's `setsparam` at params.c:3350.
3912///
3913/// Now `pub` so compsys engine ports can write `$compstate[KEY]`
3914/// directly. Also dual-writes to `paramtab_hashed_storage()` under
3915/// the "compstate" key so subscript lookups via the hash-param
3916/// machinery see the same value — `$compstate` IS a PM_HASHED param
3917/// (created by `makecompparams` at `complete.rs:1499`), and shell
3918/// scripts read it as such.
3919pub fn set_compstate_str(key: &str, val: &str) {
3920    // params.c:3350 — flat bracketed-param write (preserves the
3921    // pre-existing access path used by `set_compstate_str` callers).
3922    let pname = format!("compstate[{}]", key);
3923    let _ = setsparam(&pname, val);
3924
3925    // Hash-storage write: dual-store under the `compstate` hash so
3926    // `${compstate[KEY]}` shell reads (via the hashparam machinery)
3927    // and any direct `paramtab_hashed_storage()` consumer see the
3928    // same value.
3929    if let Ok(mut tab) = paramtab_hashed_storage().lock() {
3930        tab.entry("compstate".to_string())
3931            .or_default()
3932            .insert(key.to_string(), val.to_string());
3933    }
3934}
3935
3936/// Read `$compstate[KEY]`. Returns `None` when the key was never set.
3937///
3938/// Prefers the hash-storage view (the canonical home for a PM_HASHED
3939/// param); falls back to the legacy flat `compstate[KEY]` bracketed
3940/// param for entries that some code wrote via raw `setsparam` without
3941/// going through [`set_compstate_str`].
3942pub fn get_compstate_str(key: &str) -> Option<String> {
3943    if let Ok(tab) = paramtab_hashed_storage().lock() {
3944        if let Some(hash) = tab.get("compstate") {
3945            if let Some(v) = hash.get(key) {
3946                return Some(v.clone());
3947            }
3948        }
3949    }
3950    // Fallback: pre-existing callers wrote via raw `setsparam` only.
3951    let pname = format!("compstate[{}]", key);
3952    getsparam(&pname)
3953}
3954
3955/// Local helper: position before-the-current char (handles UTF-8).
3956#[inline]
3957fn prev_char_index(bytes: &[u8], pos: usize) -> usize {
3958    // local
3959    if pos == 0 {
3960        return 0;
3961    }
3962    let mut i = pos - 1;
3963    while i > 0 && (bytes[i] & 0xC0) == 0x80 {
3964        i -= 1;
3965    }
3966    i
3967}
3968
3969#[inline]
3970fn char_at(bytes: &[u8], pos: usize) -> char {
3971    // local
3972    if pos >= bytes.len() {
3973        return '\0';
3974    }
3975    let s = match std::str::from_utf8(&bytes[pos..]) {
3976        Ok(s) => s,
3977        Err(_) => return '\0',
3978    };
3979    s.chars().next().unwrap_or('\0')
3980}
3981
3982/// Depth counter so `set_comp_sep`'s sanity assert ("lexsave/restore
3983/// balanced") fires when a future port mismatches them.
3984static LEXSAVE_DEPTH: AtomicI32 = AtomicI32::new(0); // local
3985
3986/// Walk a balanced pair of in/out token bytes starting at `start`,
3987/// returning the index just after the closing token, or None if
3988/// unbalanced. C `skipparens` returns the position; this version
3989/// returns the same semantic.
3990fn skip_token_parens(bytes: &[u8], start: usize, open: char, close: char) -> Option<usize> {
3991    let mut depth: i32 = 0;
3992    let mut i = start;
3993    while i < bytes.len() {
3994        let c = char_at(bytes, i);
3995        if c == open {
3996            depth += 1;
3997        } else if c == close {
3998            depth -= 1;
3999            if depth == 0 {
4000                return Some(i + c.len_utf8());
4001            }
4002        }
4003        i += c.len_utf8();
4004    }
4005    if depth == 0 {
4006        Some(i)
4007    } else {
4008        None
4009    }
4010}
4011
4012/// Walk the INAMESPC name-character class — equivalent to C's
4013/// `itype_end(e, INAMESPC, 0)` loop. Stops at first non-name char.
4014fn walk_namespace(bytes: &[u8]) -> usize {
4015    // local
4016    let s = match std::str::from_utf8(bytes) {
4017        Ok(s) => s,
4018        Err(_) => return 0,
4019    };
4020    let mut len = 0usize;
4021    for c in s.chars() {
4022        if c.is_alphanumeric() || c == '_' {
4023            len += c.len_utf8();
4024        } else {
4025            break;
4026        }
4027    }
4028    len
4029}
4030
4031/// Strip Inbrace/Outbrace/Stringg/etc. token bytes back to literal
4032/// characters — substitute for C `untokenize()` over the slice. The
4033/// canonical Rust untokenize lives in `crate::lex::untokenize`.
4034fn strip_tokens(s: &str) -> String {
4035    // local
4036    crate::lex::untokenize(s).to_string()
4037}
4038
4039/// File-scope `int hcompcall` accessor — `compfunc` active iff non-empty.
4040fn compfunc_active() -> bool {
4041    compfunc
4042        .get_or_init(|| Mutex::new(None))
4043        .lock()
4044        .ok()
4045        .and_then(|g| g.clone())
4046        .map(|s| !s.is_empty())
4047        .unwrap_or(false)
4048}
4049
4050/// Direct port of `void lexsave(void)` from `Src/lex.c`. Delegates
4051/// to `zcontext_save` which pushes the lex/parse/hist context stack
4052/// frame. Returns a token (current stack depth) for symmetry with
4053/// the C `int` save token used by `set_comp_sep` for invariant check.
4054fn lexsave() -> usize {
4055    // lex.c via context.c:80
4056    crate::ported::context::zcontext_save();
4057    (LEXSAVE_DEPTH.fetch_add(1, Ordering::SeqCst) + 1) as usize
4058}
4059
4060/// Direct port of `void lexrestore(void)` from `Src/lex.c`. Pops the
4061/// last `zcontext_save` frame. C body restores hist/lex/parse via
4062/// `zcontext_restore_partial(ZCONTEXT_HIST|ZCONTEXT_LEX|ZCONTEXT_PARSE)`.
4063fn lexrestore(_token: usize) {
4064    // lex.c via context.c:117
4065    let parts = ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE;
4066    zcontext_restore_partial(parts);
4067    LEXSAVE_DEPTH.fetch_sub(1, Ordering::SeqCst);
4068}
4069
4070// ---- Extern stubs for addmatches's bucket-3 dependencies ----
4071
4072fn compquote_first() -> Option<char> {
4073    // zle_tricky.c compquote
4074    COMPQUOTE
4075        .get_or_init(|| Mutex::new(String::new()))
4076        .lock()
4077        .ok()
4078        .and_then(|g| g.chars().next())
4079}
4080fn instring_set(v: i32) {
4081    // zle_tricky.c:419
4082    INSTRING.store(v, Ordering::Relaxed);
4083}
4084fn inbackt_set(v: i32) {
4085    // zle_tricky.c:419
4086    INBACKT.store(v, Ordering::Relaxed);
4087}
4088fn autoq_set(s: &str) {
4089    // zle_tricky.c autoq
4090    if let Ok(mut g) = AUTOQ.get_or_init(|| Mutex::new(String::new())).lock() {
4091        *g = s.to_string();
4092    }
4093}
4094
4095// ---- Extern stubs for makecomplist's bucket-3 dependencies ----
4096
4097/// File-scope holder for `Cmlist bmatchers` — `Src/Zle/compcore.c:236`.
4098/// C linked-list of matchers active for brace-matching, populated by
4099/// `add_bmatchers` walking the user-installed `Cmatcher` chain.
4100pub static bmatchers: OnceLock<Mutex<Option<Box<Cmlist>>>> = OnceLock::new(); // c:236
4101
4102/// File-scope holder for `Cmlist mstack` — `Src/Zle/compcore.c:236`.
4103/// Matcher-stack — current active matcher list for compadd recursion.
4104pub static mstack: OnceLock<Mutex<Option<Box<Cmlist>>>> = OnceLock::new(); // c:236
4105
4106// ---- Extern stubs for add_match_data's Cline operations ----
4107
4108/// Bridge to `cline_matched()` — `Src/Zle/compmatch.c:253`. The
4109/// real port takes `&mut Option<Box<Cline>>` walking the chain
4110/// marking each node CLF_MATCHED. With only a string slice here we
4111/// build a one-node Cline shim and route the call through it so the
4112/// CLF_MATCHED state-machine update fires the same way as in C.
4113fn cline_matched_compcore(line: Option<&str>) {
4114    // compmatch.c:253
4115    let Some(s) = line else {
4116        return;
4117    };
4118    if s.is_empty() {
4119        return;
4120    }
4121    let mut head = Some(Box::new(Cline {
4122        line: Some(s.to_string()),
4123        llen: s.len() as i32,
4124        ..Default::default()
4125    }));
4126    cline_matched(&mut head);
4127}
4128/// Real read of `char *qisuf` via the paramtab. Mirrors C's direct
4129/// global read at `Src/Zle/zle_tricky.c qisuf`.
4130fn qisuf_get() -> String {
4131    // zle_tricky.c qisuf
4132    getsparam("qisuf").unwrap_or_default()
4133}
4134fn qipre_get() -> String {
4135    // zle_tricky.c qipre
4136    getsparam("qipre").unwrap_or_default()
4137}
4138
4139/// Adapter for `int movefd(int fd)` from `Src/utils.c:2974` —
4140/// delegates to the canonical port in `ported::utils::movefd`.
4141fn movefd(fd: i32) -> i32 {
4142    // utils.c:2974
4143    crate::ported::utils::movefd(fd)
4144}
4145
4146/// Adapter for `void redup(int new, int old)` from `Src/utils.c:2021` —
4147/// delegates to the canonical port `ported::utils::redup`. Callers
4148/// only need the new-fd form here; `old` is the inverse of movefd's
4149/// reservation (passed as -1 to mean "no original").
4150fn redup(new: i32) {
4151    // utils.c:2021
4152    crate::ported::utils::redup(new, -1);
4153}
4154
4155/// File-scope registry mirroring `Src/init.c`'s `zshhooks[]` table —
4156/// each hook name maps to the ordered list of shfunc names to call.
4157pub static HOOK_FNS: OnceLock<Mutex<std::collections::HashMap<String, Vec<String>>>> =
4158    OnceLock::new(); // init.c zshhooks
4159
4160/// Adapter for the `errflag` global from `Src/init.c` — reads the
4161/// canonical atomic in `ported::utils::errflag`.
4162fn errflag_get() -> bool {
4163    crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 // init.c
4164}
4165
4166/// Local dispatcher used by compcore call sites for hook names that
4167/// don't yet have a typed-data argument. Delegates to the canonical
4168/// `module::runhookdef(gethookdef(name), NULL)` — no-op when no
4169/// Hookfn is registered (c:993-995). Returns the Hookfn return value
4170/// (or 0 when no handler fires).
4171fn runhookdef_compcore(hook: &str) -> i32 {
4172    // c:990
4173    let h = gethookdef(hook);
4174    if h.is_null() {
4175        return 0;
4176    }
4177    runhookdef(h, std::ptr::null_mut())
4178}
4179
4180/// Direct port of `runhookdef(COMPCTLMAKEHOOK, &dat)` from
4181/// `Src/Zle/compctl.c`. The compctl module registers this hook so
4182/// `Src/Zle/compcore.c:1042-1045` dispatches into compctl's
4183/// `makecomplistctl` via its registered shfunc list.
4184fn runhookdef_compctlmake(
4185    // init.c:990 (COMPCTLMAKEHOOK)
4186    dat: &mut Ccmakedat,
4187) {
4188    // c:compctl.c:2305 makecomplistctl is the hook entrypoint.
4189    let s = dat.str.clone().unwrap_or_default();
4190    let _ = crate::ported::zle::compctl::makecomplistctl(dat.lst);
4191    let _ = s;
4192}
4193
4194// =====================================================================
4195// permmatches — `Src/Zle/compcore.c:3423`.
4196// =====================================================================
4197
4198/// Static state for `permmatches`'s `static int fi`. C scopes the
4199/// flag to the function; Rust hoists it to file scope per Rule S1.
4200static PERMMATCHES_FI: AtomicI32 = AtomicI32::new(0); // c:3423 static int fi
4201
4202/// Port of the `type==0` string-sort branch of `makearray()` from
4203/// compcore.c:3239-3257. Sorts strings via `strmetasort` + dedup.
4204pub fn makearray_strings(mut rp: Vec<String>, flags: i32) -> (Vec<String>, i32) {
4205    // c:3239
4206    let mut n: i32 = rp.len() as i32;
4207    if flags != 0 && n > 0 {
4208        // c:3240
4209        let numeric = isset(NUMERICGLOBSORT); // c:3243
4210        let mut sf = SORTIT_IGNORING_BACKSLASHES as u32;
4211        if numeric {
4212            sf |= SORTIT_NUMERICALLY as u32;
4213        }
4214        crate::ported::sort::strmetasort(&mut rp, sf, None); // c:3242-3244
4215
4216        // Dedup consecutive equals.                                         // c:3247
4217        let mut cp = 0usize;
4218        let mut ap = 0usize;
4219        while ap < rp.len() {
4220            if ap != cp {
4221                rp.swap(ap, cp);
4222            }
4223            cp += 1;
4224            let mut bp = ap;
4225            while bp + 1 < rp.len() && rp[ap] == rp[bp + 1] {
4226                // c:3250
4227                bp += 1;
4228                n -= 1;
4229            }
4230            ap = bp + 1; // c:3252
4231        }
4232        rp.truncate(cp); // c:3253
4233    }
4234    (rp, n)
4235}
4236
4237#[cfg(test)]
4238mod tests {
4239    use super::*;
4240
4241    #[test]
4242    fn rembslash_basic() {
4243        let _g = crate::test_util::global_state_lock();
4244        let _g = zle_test_setup();
4245        assert_eq!(rembslash("hello\\ world"), "hello world");
4246        assert_eq!(rembslash("no\\\\slash"), "no\\slash");
4247        assert_eq!(rembslash("plain"), "plain");
4248    }
4249
4250    #[test]
4251    fn comp_quoting_string_table() {
4252        let _g = crate::test_util::global_state_lock();
4253        let _g = zle_test_setup();
4254        assert_eq!(comp_quoting_string(QT_SINGLE), "'");
4255        assert_eq!(comp_quoting_string(QT_DOUBLE), "\"");
4256        assert_eq!(comp_quoting_string(QT_DOLLARS), "$'");
4257        assert_eq!(comp_quoting_string(0), "\\");
4258        assert_eq!(comp_quoting_string(QT_BACKSLASH), "\\");
4259    }
4260
4261    #[test]
4262    fn matcheq_equal_strings() {
4263        let _g = crate::test_util::global_state_lock();
4264        let _g = zle_test_setup();
4265        let mut a = Cmatch::default();
4266        a.str = Some("foo".into());
4267        let mut b = Cmatch::default();
4268        b.str = Some("foo".into());
4269        assert!(matcheq(&a, &b));
4270    }
4271
4272    #[test]
4273    fn matcheq_different_strings() {
4274        let _g = crate::test_util::global_state_lock();
4275        let _g = zle_test_setup();
4276        let mut a = Cmatch::default();
4277        a.str = Some("foo".into());
4278        let mut b = Cmatch::default();
4279        b.str = Some("bar".into());
4280        assert!(!matcheq(&a, &b));
4281    }
4282
4283    #[test]
4284    fn matcheq_one_side_none() {
4285        let _g = crate::test_util::global_state_lock();
4286        let _g = zle_test_setup();
4287        let mut a = Cmatch::default();
4288        a.pre = Some("p".into());
4289        let b = Cmatch::default();
4290        assert!(!matcheq(&a, &b));
4291    }
4292
4293    #[test]
4294    fn get_user_var_reads_array_from_paramtab() {
4295        let _g = crate::test_util::global_state_lock();
4296        // c:2003 — `getaparam(nam)` first. Verify array params come
4297        //          out as a Vec, not via env.
4298        let _g = zle_test_setup();
4299        setaparam("__test_arr", vec!["a".into(), "bb".into(), "ccc".into()]);
4300        let got = get_user_var(Some("__test_arr"));
4301        assert_eq!(got, Some(vec!["a".into(), "bb".into(), "ccc".into()]));
4302        // Cleanup so we don't poison other tests.
4303        setaparam("__test_arr", vec![]);
4304    }
4305
4306    #[test]
4307    fn get_user_var_reads_scalar_as_single_element_array() {
4308        let _g = crate::test_util::global_state_lock();
4309        // c:2007-2009 — getsparam fallback: wrap scalar in 1-element array.
4310        let _g = zle_test_setup();
4311        setsparam("__test_scalar", "hello");
4312        let got = get_user_var(Some("__test_scalar"));
4313        assert_eq!(got, Some(vec!["hello".to_string()]));
4314        setsparam("__test_scalar", "");
4315    }
4316
4317    #[test]
4318    fn get_user_var_paren_list_splits_on_separators() {
4319        let _g = crate::test_util::global_state_lock();
4320        // c:1960-1996 — `(a b c)` paren list, NOT a param lookup.
4321        let _g = zle_test_setup();
4322        let got = get_user_var(Some("(one two three)"));
4323        assert_eq!(got, Some(vec!["one".into(), "two".into(), "three".into()]));
4324    }
4325
4326    #[test]
4327    fn get_user_var_none_for_missing() {
4328        let _g = crate::test_util::global_state_lock();
4329        // c:1956 + c:2009 — missing param returns None.
4330        let _g = zle_test_setup();
4331        // (env vars must not leak through — we don't read $PATH etc.)
4332        let got = get_user_var(Some("__definitely_not_a_param_xyz"));
4333        assert_eq!(got, None);
4334    }
4335
4336    #[test]
4337    fn get_data_arr_reads_hashed_keys_or_values() {
4338        let _g = crate::test_util::global_state_lock();
4339        // c:2022 — fetchvalue(name, SCANPM_WANTKEYS|WANTVALS|MATCHMANY).
4340        let _g = zle_test_setup();
4341        crate::ported::params::sethparam(
4342            "__test_hash",
4343            vec!["k1".into(), "v1".into(), "k2".into(), "v2".into()],
4344        );
4345
4346        let keys = get_data_arr("__test_hash", true);
4347        assert!(keys.is_some(), "hashed param should have keys");
4348        let mut keys = keys.unwrap();
4349        keys.sort();
4350        assert_eq!(keys, vec!["k1".to_string(), "k2".to_string()]);
4351
4352        let vals = get_data_arr("__test_hash", false);
4353        assert!(vals.is_some(), "hashed param should have values");
4354        let mut vals = vals.unwrap();
4355        vals.sort();
4356        assert_eq!(vals, vec!["v1".to_string(), "v2".to_string()]);
4357    }
4358
4359    #[test]
4360    fn get_data_arr_none_for_non_hashed() {
4361        let _g = crate::test_util::global_state_lock();
4362        // c:2032 — fetchvalue NULL → return NULL for params that
4363        //          aren't associative arrays.
4364        let _g = zle_test_setup();
4365        setsparam("__test_scalar2", "value");
4366        let got = get_data_arr("__test_scalar2", false);
4367        assert_eq!(got, None, "scalar params must NOT come out of get_data_arr");
4368    }
4369
4370    #[test]
4371    fn before_complete_snapshots_oldmenucmp() {
4372        let _g = crate::test_util::global_state_lock();
4373        // c:463 — `oldmenucmp = menucmp;`
4374        let _g = zle_test_setup();
4375        MENUCMP.store(7, Ordering::Relaxed);
4376        OLDMENUCMP.store(0, Ordering::Relaxed);
4377        let mut lst = 0;
4378        let _ = before_complete(&mut lst);
4379        assert_eq!(OLDMENUCMP.load(Ordering::Relaxed), 7);
4380        // Reset for other tests.
4381        MENUCMP.store(0, Ordering::Relaxed);
4382        OLDMENUCMP.store(0, Ordering::Relaxed);
4383    }
4384
4385    #[test]
4386    fn before_complete_clears_showagain() {
4387        let _g = crate::test_util::global_state_lock();
4388        // c:467 — `showagain = 0;` always (after the validlist gate).
4389        let _g = zle_test_setup();
4390        SHOWAGAIN.store(5, Ordering::Relaxed);
4391        let mut lst = 0;
4392        let _ = before_complete(&mut lst);
4393        assert_eq!(
4394            SHOWAGAIN.load(Ordering::Relaxed),
4395            0,
4396            "SHOWAGAIN must be cleared by before_complete"
4397        );
4398    }
4399
4400    #[test]
4401    fn remsquote_default_quoting() {
4402        let _g = crate::test_util::global_state_lock();
4403        let _g = zle_test_setup();
4404        let mut s = String::from("a'\\''b");
4405        let n = remsquote(&mut s);
4406        assert_eq!(s, "a'b");
4407        assert_eq!(n, 3);
4408    }
4409
4410    #[test]
4411    fn ctokenize_dollar_substitution() {
4412        let _g = crate::test_util::global_state_lock();
4413        let _g = zle_test_setup();
4414        let out = ctokenize("$x{y}");
4415        let chars: Vec<char> = out.chars().collect();
4416        assert_eq!(chars[0], Stringg);
4417        assert_eq!(chars[1], 'x');
4418        assert_eq!(chars[2], Inbrace);
4419        assert_eq!(chars[3], 'y');
4420        assert_eq!(chars[4], Outbrace);
4421    }
4422
4423    #[test]
4424    fn get_user_var_inline_list() {
4425        let _g = crate::test_util::global_state_lock();
4426        let _g = zle_test_setup();
4427        let result = get_user_var(Some("(a b c)")).unwrap();
4428        assert_eq!(result, vec!["a", "b", "c"]);
4429    }
4430
4431    #[test]
4432    fn matchcmp_str_sort_default() {
4433        let _g = crate::test_util::global_state_lock();
4434        let _g = zle_test_setup();
4435        MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
4436        let mut a = Cmatch::default();
4437        a.str = Some("apple".into());
4438        let mut b = Cmatch::default();
4439        b.str = Some("banana".into());
4440        assert_eq!(matchcmp(&a, &b), std::cmp::Ordering::Less);
4441        assert_eq!(matchcmp(&b, &a), std::cmp::Ordering::Greater);
4442        assert_eq!(matchcmp(&a, &a), std::cmp::Ordering::Equal);
4443        MATCHORDER.store(0, Ordering::Relaxed);
4444    }
4445
4446    #[test]
4447    fn dupmatch_clones_strings_and_truncates_braces() {
4448        let _g = crate::test_util::global_state_lock();
4449        let _g = zle_test_setup();
4450        // C body c:3370: deep-copy strings, truncate brpl/brsl to nbeg/nend.
4451        let mut src = Cmatch::default();
4452        src.str = Some("foo".into());
4453        src.ipre = Some("ipre".into());
4454        src.flags = 7;
4455        src.brpl = vec![10, 20, 30, 40];
4456        src.brsl = vec![5, 6, 7];
4457        src.qipl = 1;
4458        src.qisl = 2;
4459        src.mode = 0o755;
4460        src.modec = 'd';
4461
4462        let r = dupmatch(&src, 2, 1);
4463        assert_eq!(r.str.as_deref(), Some("foo"));
4464        assert_eq!(r.ipre.as_deref(), Some("ipre"));
4465        assert_eq!(r.flags, 7);
4466        assert_eq!(r.brpl, vec![10, 20]); // truncated to nbeg=2
4467        assert_eq!(r.brsl, vec![5]); // truncated to nend=1
4468        assert_eq!(r.qipl, 1);
4469        assert_eq!(r.qisl, 2);
4470        assert_eq!(r.mode, 0o755);
4471        assert_eq!(r.modec, 'd');
4472    }
4473
4474    #[test]
4475    fn dupmatch_empty_braces_stay_empty() {
4476        let _g = crate::test_util::global_state_lock();
4477        let _g = zle_test_setup();
4478        // C body c:3395/3404: NULL brpl/brsl stay NULL regardless of nbeg/nend.
4479        let src = Cmatch::default();
4480        let r = dupmatch(&src, 5, 5);
4481        assert!(r.brpl.is_empty());
4482        assert!(r.brsl.is_empty());
4483    }
4484
4485    #[test]
4486    fn makearray_sorted_and_deduped() {
4487        let _g = crate::test_util::global_state_lock();
4488        let _g = zle_test_setup();
4489        // c:3262-3291: sort + dedup with matcheq. Same str + nil disp =>
4490        // collapses into one entry with CMF_FMULT set on the survivor.
4491        let mut a = Cmatch::default();
4492        a.str = Some("z".into());
4493        let mut b = Cmatch::default();
4494        b.str = Some("a".into());
4495        let mut c = Cmatch::default();
4496        c.str = Some("a".into());
4497        let (arr, n, _nl, _ll) = makearray(vec![a, b, c], CGF_MATSORT);
4498        // Two distinct visible strings after dedup ("a", "z").
4499        assert_eq!(arr.len(), 2);
4500        assert_eq!(n, 2);
4501        assert_eq!(arr[0].str.as_deref(), Some("a"));
4502        assert_eq!(arr[1].str.as_deref(), Some("z"));
4503    }
4504
4505    #[test]
4506    fn makearray_nosort_unchanged_order() {
4507        let _g = crate::test_util::global_state_lock();
4508        let _g = zle_test_setup();
4509        // c:3300: CGF_NOSORT branch; with no UNIQ flags, order preserved.
4510        let mut a = Cmatch::default();
4511        a.str = Some("z".into());
4512        let mut b = Cmatch::default();
4513        b.str = Some("a".into());
4514        let (arr, n, _, _) = makearray(vec![a, b], CGF_NOSORT | CGF_UNIQALL);
4515        // UNIQALL active so no dedup pass runs.
4516        assert_eq!(n, 2);
4517        assert_eq!(arr[0].str.as_deref(), Some("z"));
4518        assert_eq!(arr[1].str.as_deref(), Some("a"));
4519    }
4520
4521    #[test]
4522    fn makearray_strings_dedup_consecutive() {
4523        let _g = crate::test_util::global_state_lock();
4524        let _g = zle_test_setup();
4525        // c:3239 path: sort + drop adjacent duplicates.
4526        let (arr, n) = makearray_strings(vec!["b".into(), "a".into(), "a".into(), "c".into()], 1);
4527        assert_eq!(n, 3);
4528        assert_eq!(arr, vec!["a", "b", "c"]);
4529    }
4530
4531    #[test]
4532    fn check_param_no_dollar_returns_none() {
4533        let _g = crate::test_util::global_state_lock();
4534        let _g = zle_test_setup();
4535        // c:1316: no `$` in string → return None.
4536        OFFS.store(2, Ordering::Relaxed);
4537        assert_eq!(check_param("abc", false, false), None);
4538    }
4539
4540    #[test]
4541    fn check_param_simple_dollar_var_at_cursor() {
4542        let _g = crate::test_util::global_state_lock();
4543        let _g = zle_test_setup();
4544        // c:1259-1311: `$FOO` with cursor inside the name → return b.
4545        OFFS.store(2, Ordering::Relaxed);
4546        let s = format!("{}FOO", Stringg);
4547        let r = check_param(&s, false, true);
4548        assert!(r.is_some(), "expected Some(b) inside $FOO");
4549    }
4550
4551    #[test]
4552    fn callcompfunc_empty_fn_no_panic() {
4553        let _g = crate::test_util::global_state_lock();
4554        let _g = zle_test_setup();
4555        // c:552: getshfunc(NULL) early-return.
4556        callcompfunc("anything", "");
4557    }
4558
4559    #[test]
4560    fn callcompfunc_sets_compstate_context() {
4561        let _g = crate::test_util::global_state_lock();
4562        let _g = zle_test_setup();
4563        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4564        // c:619: context selection — verified via the pure
4565        // compcontext_for helper (callcompfunc calls it and writes
4566        // to paramtab via setsparam, but paramtab read-back in a
4567        // unit-test context without a live VM is unreliable).
4568        ispar.store(0, Ordering::Relaxed);
4569        linwhat.store(IN_PAR_LW, Ordering::Relaxed);
4570        assert_eq!(compcontext_for("foo"), "assign_parameter");
4571        // Body executes without panicking against the real paramtab.
4572        callcompfunc("foo", "_test_fn");
4573    }
4574
4575    /// Test-only serializer for tests that mutate file-scope globals.
4576    static GLOBAL_MUT_LOCK: Mutex<()> = Mutex::new(());
4577
4578    #[test]
4579    fn compcontext_for_routes_ispar_first() {
4580        let _g = crate::test_util::global_state_lock();
4581        let _g = zle_test_setup();
4582        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4583        ispar.store(2, Ordering::Relaxed);
4584        linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
4585        assert_eq!(compcontext_for("x"), "brace_parameter");
4586        ispar.store(1, Ordering::Relaxed);
4587        assert_eq!(compcontext_for("x"), "parameter");
4588        ispar.store(0, Ordering::Relaxed);
4589        linwhat.store(IN_MATH_LW, Ordering::Relaxed);
4590        assert_eq!(compcontext_for("x"), "math");
4591        linwhat.store(IN_COND_LW, Ordering::Relaxed);
4592        assert_eq!(compcontext_for("x"), "condition");
4593        linwhat.store(IN_ENV_LW, Ordering::Relaxed);
4594        assert_eq!(compcontext_for("x"), "value");
4595        linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
4596        assert_eq!(compcontext_for("x"), "command");
4597    }
4598
4599    #[test]
4600    fn addmatches_empty_argv_early_return() {
4601        let _g = crate::test_util::global_state_lock();
4602        let _g = zle_test_setup();
4603        // c:2138-2139: empty argv + dummies==0 + no CAF_ALL → return 1.
4604        let mut dat = Cadata::default();
4605        dat.dummies = 0;
4606        dat.aflags = 0;
4607        assert_eq!(addmatches(&mut dat, &[]), 1);
4608    }
4609
4610    #[test]
4611    fn addmatches_appends_argv_to_default_group() {
4612        let _g = crate::test_util::global_state_lock();
4613        let _g = zle_test_setup();
4614        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4615        // c:2200 simplified body: each argv entry → addmatch into "default" group.
4616        amatches
4617            .get_or_init(|| Mutex::new(Vec::new()))
4618            .lock()
4619            .unwrap()
4620            .clear();
4621        matches
4622            .get_or_init(|| Mutex::new(Vec::new()))
4623            .lock()
4624            .unwrap()
4625            .clear();
4626        let mut dat = Cadata::default();
4627        dat.dummies = -1;
4628        let _ = addmatches(&mut dat, &["a".into(), "b".into()]);
4629        let n = matches.get().unwrap().lock().unwrap().len();
4630        assert!(n >= 2);
4631    }
4632
4633    #[test]
4634    fn add_match_data_returns_populated_cmatch() {
4635        let _g = crate::test_util::global_state_lock();
4636        let _g = zle_test_setup();
4637        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4638        // c:3052-3067: cm.str/orig/pre/suf populated; mnum bumps by 1.
4639        matches
4640            .get_or_init(|| Mutex::new(Vec::new()))
4641            .lock()
4642            .unwrap()
4643            .clear();
4644        let before = mnum.load(Ordering::Relaxed);
4645        let cm = add_match_data(
4646            0,
4647            "match",
4648            "match-orig",
4649            None,
4650            "ipre",
4651            "ripre",
4652            "isuf",
4653            "pre",
4654            "prpre",
4655            "ppre",
4656            None,
4657            "psuf",
4658            None,
4659            "suf",
4660            0,
4661            0,
4662        );
4663        assert_eq!(cm.str.as_deref(), Some("match"));
4664        assert_eq!(cm.orig.as_deref(), Some("match-orig"));
4665        assert_eq!(cm.pre.as_deref(), Some("pre"));
4666        assert_eq!(cm.suf.as_deref(), Some("suf"));
4667        assert_eq!(mnum.load(Ordering::Relaxed), before + 1);
4668    }
4669
4670    #[test]
4671    fn add_match_data_exact_records_into_ainfo() {
4672        let _g = crate::test_util::global_state_lock();
4673        let _g = zle_test_setup();
4674        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4675        // c:3037-3058: exact != 0 writes `ai->exact = useexact` and
4676        // `ai->exactm = cm`. The test sets useexact=1 to exercise the
4677        // accept-exact path.
4678        let saved_useexact = useexact.load(Ordering::Relaxed);
4679        useexact.store(1, Ordering::Relaxed);
4680        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4681            *g = Some(Aminfo::default());
4682        }
4683        let _ = add_match_data(
4684            0, "x", "x", None, "", "", "", "", "", "", None, "", None, "", 0, 1,
4685        );
4686        let a = ainfo.get().unwrap().lock().unwrap().clone().unwrap();
4687        useexact.store(saved_useexact, Ordering::Relaxed);
4688        assert_eq!(a.exact, 1);
4689        assert!(a.exactm.is_some());
4690    }
4691
4692    #[test]
4693    fn set_comp_sep_returns_one() {
4694        let _g = crate::test_util::global_state_lock();
4695        let _g = zle_test_setup();
4696        // c:1937: stubbed body returns 1 (no-change marker).
4697        assert_eq!(set_comp_sep(), 1);
4698    }
4699
4700    #[test]
4701    fn foredel_deletes_forward_from_zlemetacs() {
4702        let _g = crate::test_util::global_state_lock();
4703        let _g = zle_test_setup();
4704        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4705        // zle_utils.c:1105 — delete `ct` chars forward from ZLEMETACS.
4706        if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
4707            *g = "abcdef".to_string();
4708        }
4709        ZLEMETACS.store(2, Ordering::Relaxed);
4710        ZLEMETALL.store(6, Ordering::Relaxed);
4711        foredel(3, CUT_RAW);
4712        let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
4713        assert_eq!(line, "abf");
4714        assert_eq!(ZLEMETALL.load(Ordering::Relaxed), 3);
4715    }
4716
4717    #[test]
4718    fn inststr_inserts_at_zlemetacs() {
4719        let _g = crate::test_util::global_state_lock();
4720        let _g = zle_test_setup();
4721        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4722        // zle_tricky.c:278 — insert at cursor.
4723        if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
4724            *g = "hello".to_string();
4725        }
4726        ZLEMETACS.store(5, Ordering::Relaxed);
4727        ZLEMETALL.store(5, Ordering::Relaxed);
4728        let _ = inststr(" world");
4729        let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
4730        assert_eq!(line, "hello world");
4731        assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 11);
4732    }
4733
4734    #[test]
4735    fn metafy_and_unmetafy_roundtrip_globals() {
4736        let _g = crate::test_util::global_state_lock();
4737        let _g = zle_test_setup();
4738        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4739        // zle_tricky.c:978,995 — meta/unmeta operate on the global pair.
4740        if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
4741            *g = "plain ascii".to_string();
4742        }
4743        ZLECS.store(3, Ordering::Relaxed);
4744        ZLELL.store(11, Ordering::Relaxed);
4745        metafy_line();
4746        // For ASCII input the meta form equals the raw form.
4747        assert_eq!(
4748            ZLEMETALINE.get().unwrap().lock().unwrap().clone(),
4749            "plain ascii"
4750        );
4751        assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 3);
4752        unmetafy_line();
4753        assert_eq!(
4754            ZLELINE.get().unwrap().lock().unwrap().clone(),
4755            "plain ascii"
4756        );
4757    }
4758
4759    #[test]
4760    fn selfinsert_appends_lastchar_at_zlecs() {
4761        let _g = crate::test_util::global_state_lock();
4762        let _g = zle_test_setup();
4763        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4764        // zle_misc.c:112-141 — insert one char at cursor, bump zlecs.
4765        //
4766        // `selfinsert()` (zle_misc.rs:180) writes through
4767        // `self_insert(c)` which mutates the `zle_main::ZLELINE`
4768        // (`Mutex<Vec<char>>`) plus `zle_main::ZLECS`/`ZLELL` —
4769        // NOT the `compcore::ZLELINE` (`OnceLock<Mutex<String>>`)
4770        // used by the meta/unmeta tests above. The original test
4771        // seeded the wrong buffer, so the assert kept seeing "ab"
4772        // (the compcore buffer never received the insert) while
4773        // self_insert silently appended 'c' to the zle_main buffer.
4774        //
4775        // Set up the zle_main buffer for the insert, then read back
4776        // from there. `zle_test_setup` already clears the zle_main
4777        // statics, so we start from a known zero state.
4778        {
4779            let mut g = crate::ported::zle::zle_main::ZLELINE.lock().unwrap();
4780            *g = "ab".chars().collect();
4781        }
4782        crate::ported::zle::zle_main::ZLECS.store(2, Ordering::Relaxed);
4783        crate::ported::zle::zle_main::ZLELL.store(2, Ordering::Relaxed);
4784        LASTCHAR.store(b'c' as i32, Ordering::Relaxed);
4785        // Force the wide-char re-derive path (lastchar_wide_valid=0 →
4786        // selfinsert refills it from LASTCHAR per zle_misc.c:119-122).
4787        LASTCHAR_WIDE_VALID.store(0, Ordering::Relaxed);
4788        let rv = selfinsert(&[]);
4789        assert_eq!(rv, 0);
4790        let buf: String = crate::ported::zle::zle_main::ZLELINE
4791            .lock()
4792            .unwrap()
4793            .iter()
4794            .collect();
4795        assert_eq!(buf, "abc");
4796        assert_eq!(
4797            crate::ported::zle::zle_main::ZLECS.load(Ordering::Relaxed),
4798            3,
4799        );
4800    }
4801
4802    #[test]
4803    fn minfo_clear_and_asked_zero_mutate_state() {
4804        let _g = crate::test_util::global_state_lock();
4805        let _g = zle_test_setup();
4806        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4807        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4808            let mut cm = Cmatch::default();
4809            cm.str = Some("x".into());
4810            g.cur = Some(Box::new(cm));
4811            g.asked = 1;
4812        }
4813        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4814            g.cur = None;
4815        }
4816        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4817            g.asked = 0;
4818        }
4819        let m = MINFO.get().unwrap().lock().unwrap().clone();
4820        assert!(m.cur.is_none());
4821        assert_eq!(m.asked, 0);
4822    }
4823
4824    #[test]
4825    fn cline_matched_stub_marks_node() {
4826        let _g = crate::test_util::global_state_lock();
4827        let _g = zle_test_setup();
4828        // compmatch.c:253 — sets CLF_MATCHED on the node chain. We
4829        // verify by running through the stub on a non-empty string
4830        // without panicking and trusting compmatch's body for the
4831        // actual flag set.
4832        cline_matched_compcore(Some("foo"));
4833        cline_matched_compcore(None);
4834        cline_matched_compcore(Some(""));
4835    }
4836
4837    #[test]
4838    fn permmatches_returns_fi_zero_when_count_present() {
4839        let _g = crate::test_util::global_state_lock();
4840        let _g = zle_test_setup();
4841        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
4842        // c:3444-3447: if ainfo->count is non-zero, fi stays 0.
4843        amatches
4844            .get_or_init(|| Mutex::new(Vec::new()))
4845            .lock()
4846            .unwrap()
4847            .clear();
4848        pmatches
4849            .get_or_init(|| Mutex::new(Vec::new()))
4850            .lock()
4851            .unwrap()
4852            .clear();
4853        if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4854            *a = Some(Aminfo {
4855                count: 5,
4856                ..Default::default()
4857            });
4858        }
4859        newmatches.store(1, Ordering::Relaxed);
4860        let fi = permmatches(0);
4861        assert_eq!(fi, 0);
4862        assert_eq!(hasperm.load(Ordering::Relaxed), 1);
4863    }
4864
4865    /// c:1323 — `rembslash` removes backslash escapes by walking the
4866    /// string and dropping every `\` while keeping its successor.
4867    /// `\\\\` (literal `\\`) → `\` (single backslash); `\a` → `a`.
4868    /// A regression that drops the successor too would silently strip
4869    /// real chars from `path/to/\$file`.
4870    #[test]
4871    fn rembslash_unescapes_canonical_pairs() {
4872        let _g = crate::test_util::global_state_lock();
4873        assert_eq!(rembslash(r"\a"), "a");
4874        assert_eq!(rembslash(r"\\"), r"\");
4875        assert_eq!(rembslash(r"\$foo"), "$foo");
4876        assert_eq!(rembslash("plain"), "plain");
4877    }
4878
4879    /// c:1323 — empty input → empty output (the loop never runs).
4880    /// Catches a regression that returns " " or "\0" for empty input.
4881    #[test]
4882    fn rembslash_empty_input_returns_empty() {
4883        let _g = crate::test_util::global_state_lock();
4884        assert_eq!(rembslash(""), "");
4885    }
4886
4887    /// c:1323 — trailing lone `\` MUST silently drop (no following
4888    /// char to keep). C's pattern `if (let Some) { push }` is
4889    /// equivalent. Regression that pushes the literal `\` would break
4890    /// shell paths with trailing backslashes (rare but legal).
4891    #[test]
4892    fn rembslash_trailing_lone_backslash_drops_silently() {
4893        let _g = crate::test_util::global_state_lock();
4894        assert_eq!(rembslash(r"foo\"), "foo");
4895    }
4896
4897    /// c:1366 — `ctokenize` is the inverse of `untokenize`: escapes
4898    /// shell metacharacters into their tokenised forms used by the
4899    /// completion machinery. Plain alphanumerics pass through.
4900    #[test]
4901    fn ctokenize_passes_alphanumerics_through() {
4902        let _g = crate::test_util::global_state_lock();
4903        assert_eq!(ctokenize("foo123"), "foo123");
4904        assert_eq!(ctokenize(""), "");
4905        assert_eq!(ctokenize("path/to"), "path/to");
4906    }
4907
4908    /// c:1435 — `comp_quoting_string` returns one of the canonical
4909    /// quote strings: `'`, `"`, `$'`, or "" depending on `stype`.
4910    /// Catches a regression where the dispatch returns the wrong
4911    /// quote — completion would generate `cmd 'arg"` (mismatched).
4912    #[test]
4913    fn comp_quoting_string_dispatches_known_styles() {
4914        let _g = crate::test_util::global_state_lock();
4915        // The exact stype values are private to the completion impl,
4916        // but the function MUST return a non-panicking string for
4917        // every reasonable input. Probe a few values.
4918        for stype in 0..=8 {
4919            let _ = comp_quoting_string(stype);
4920        }
4921    }
4922
4923    /// c:1065 — `multiquote` with empty COMPQSTACK is a no-op (returns
4924    /// input unchanged). The stack is the per-completion quoting
4925    /// context; outside completion it's empty. Regression that quotes
4926    /// regardless would corrupt every non-completion caller.
4927    #[test]
4928    fn multiquote_empty_stack_returns_input_unchanged() {
4929        let _g = crate::test_util::global_state_lock();
4930        // Reset COMPQSTACK to empty.
4931        if let Some(c) = COMPQSTACK.get() {
4932            if let Ok(mut g) = c.lock() {
4933                g.clear();
4934            }
4935        }
4936        assert_eq!(multiquote("hello", 0), "hello");
4937        assert_eq!(multiquote("", 0), "");
4938    }
4939
4940    /// c:1092 — `tildequote("foo")` (no leading ~) MUST behave like
4941    /// multiquote — the tilde-special path is a no-op when there's no
4942    /// `~` to protect. Regression that always strips/restores would
4943    /// silently mangle non-tilde inputs.
4944    #[test]
4945    fn tildequote_non_tilde_input_unchanged() {
4946        let _g = crate::test_util::global_state_lock();
4947        // Empty COMPQSTACK + no ~ → input unchanged.
4948        if let Some(c) = COMPQSTACK.get() {
4949            if let Ok(mut g) = c.lock() {
4950                g.clear();
4951            }
4952        }
4953        assert_eq!(tildequote("foo/bar", 0), "foo/bar");
4954    }
4955
4956    /// c:1092 — empty input through tildequote is empty out.
4957    #[test]
4958    fn tildequote_empty_input_empty_output() {
4959        let _g = crate::test_util::global_state_lock();
4960        if let Some(c) = COMPQSTACK.get() {
4961            if let Ok(mut g) = c.lock() {
4962                g.clear();
4963            }
4964        }
4965        assert_eq!(tildequote("", 0), "");
4966    }
4967
4968    // ─── zsh-corpus pins for rembslash ─────────────────────────────
4969
4970    /// `rembslash("\\a\\b\\c")` strips backslashes → "abc".
4971    #[test]
4972    fn compcore_corpus_rembslash_strips_escapes() {
4973        let _g = crate::test_util::global_state_lock();
4974        assert_eq!(rembslash(r"\a\b\c"), "abc");
4975    }
4976
4977    /// `rembslash` with no backslashes is identity.
4978    #[test]
4979    fn compcore_corpus_rembslash_no_escapes_identity() {
4980        let _g = crate::test_util::global_state_lock();
4981        assert_eq!(rembslash("hello"), "hello");
4982    }
4983
4984    /// `rembslash("")` returns empty string.
4985    #[test]
4986    fn compcore_corpus_rembslash_empty_is_empty() {
4987        let _g = crate::test_util::global_state_lock();
4988        assert_eq!(rembslash(""), "");
4989    }
4990
4991    /// `rembslash("\\\\")` (escaped backslash) returns "\\".
4992    #[test]
4993    fn compcore_corpus_rembslash_escaped_backslash() {
4994        let _g = crate::test_util::global_state_lock();
4995        // r"\\" in Rust is the 2-char string "\\\\"  → one literal backslash + one literal backslash
4996        assert_eq!(rembslash(r"\\"), r"\");
4997    }
4998
4999    /// `rembslash` mid-string escape preserves surroundings.
5000    #[test]
5001    fn compcore_corpus_rembslash_preserves_context() {
5002        let _g = crate::test_util::global_state_lock();
5003        assert_eq!(rembslash(r"hello\ world"), "hello world");
5004    }
5005
5006    /// Trailing single backslash is consumed (no char after).
5007    #[test]
5008    fn compcore_corpus_rembslash_trailing_backslash_consumed() {
5009        let _g = crate::test_util::global_state_lock();
5010        assert_eq!(rembslash(r"abc\"), "abc");
5011    }
5012
5013    // ═══════════════════════════════════════════════════════════════════
5014    // Additional C-parity tests for Src/Zle/compcore.c rembslash + remsquote
5015    // + ctokenize + multiquote / tildequote string transforms.
5016    // ═══════════════════════════════════════════════════════════════════
5017
5018    /// c:1323 — `rembslash("abc")` (no backslash) is identity.
5019    #[test]
5020    fn rembslash_no_backslash_is_identity() {
5021        let _g = crate::test_util::global_state_lock();
5022        assert_eq!(rembslash("abc"), "abc");
5023        assert_eq!(rembslash("hello world"), "hello world");
5024    }
5025
5026    /// c:1323 — `rembslash` of single backslash + char drops the backslash.
5027    #[test]
5028    fn rembslash_drops_backslash_keeps_next() {
5029        let _g = crate::test_util::global_state_lock();
5030        assert_eq!(rembslash(r"\a"), "a");
5031        assert_eq!(rembslash(r"\."), ".");
5032        assert_eq!(rembslash(r"\$"), "$");
5033    }
5034
5035    /// c:1323 — repeated escapes: every `\X` collapses to `X`.
5036    #[test]
5037    fn rembslash_multiple_escapes_chain() {
5038        let _g = crate::test_util::global_state_lock();
5039        assert_eq!(rembslash(r"\a\b\c"), "abc");
5040    }
5041
5042    /// c:1343 — `remsquote("")` returns 0 (no chars consumed).
5043    #[test]
5044    fn remsquote_empty_returns_zero() {
5045        let _g = crate::test_util::global_state_lock();
5046        let mut s = String::new();
5047        let r = remsquote(&mut s);
5048        assert_eq!(r, 0);
5049        assert_eq!(s, "");
5050    }
5051
5052    /// c:1343 — `remsquote("abc")` (no quote sequences) is identity,
5053    /// returns 0.
5054    #[test]
5055    fn remsquote_no_quotes_is_identity() {
5056        let _g = crate::test_util::global_state_lock();
5057        let mut s = String::from("hello world");
5058        let r = remsquote(&mut s);
5059        assert_eq!(r, 0, "no quote sequences → 0");
5060        assert_eq!(s, "hello world", "string unchanged");
5061    }
5062
5063    /// c:1366 — `ctokenize("")` returns empty string.
5064    #[test]
5065    fn ctokenize_empty_returns_empty() {
5066        let _g = crate::test_util::global_state_lock();
5067        assert_eq!(ctokenize(""), "");
5068    }
5069
5070    /// c:1366 — `ctokenize("abc")` (no special chars) preserves content.
5071    #[test]
5072    fn ctokenize_plain_ascii_preserved() {
5073        let _g = crate::test_util::global_state_lock();
5074        // No $, {, }, or backslash → byte-for-byte preservation.
5075        let r = ctokenize("abc");
5076        assert_eq!(r.as_bytes()[0], b'a');
5077        assert_eq!(r.as_bytes()[1], b'b');
5078        assert_eq!(r.as_bytes()[2], b'c');
5079    }
5080
5081    /// c:1505 — `comp_quoting_string(0)` returns a static str (no panic).
5082    #[test]
5083    fn comp_quoting_string_returns_static_str_for_all_stypes() {
5084        let _g = crate::test_util::global_state_lock();
5085        for stype in 0..10 {
5086            let _s = comp_quoting_string(stype);
5087            // No panic = pass; returned &'static str is well-defined.
5088        }
5089    }
5090
5091    /// c:954 — `multiquote("")` returns empty.
5092    #[test]
5093    fn multiquote_empty_returns_empty() {
5094        let _g = crate::test_util::global_state_lock();
5095        let _g2 = zle_test_setup();
5096        let r = multiquote("", 0);
5097        assert_eq!(r, "");
5098    }
5099
5100    /// c:980 — `tildequote("")` returns empty.
5101    #[test]
5102    fn tildequote_empty_returns_empty() {
5103        let _g = crate::test_util::global_state_lock();
5104        let _g2 = zle_test_setup();
5105        let r = tildequote("", 0);
5106        assert_eq!(r, "");
5107    }
5108
5109    /// c:980 — `tildequote("plain")` (no tilde) is identity.
5110    #[test]
5111    fn tildequote_no_tilde_is_identity() {
5112        let _g = crate::test_util::global_state_lock();
5113        let _g2 = zle_test_setup();
5114        let r = tildequote("plain", 0);
5115        assert_eq!(r, "plain", "no tilde → input unchanged");
5116    }
5117
5118    // ═══════════════════════════════════════════════════════════════════
5119    // Additional C-parity tests for Src/Zle/compcore.c
5120    // c:936 multiquote / c:972 tildequote / c:1014 check_param /
5121    // c:1315 rembslash / c:1338 remsquote / c:1381 ctokenize /
5122    // c:1478 comp_quoting_string / c:2809 matchcmp / c:2872 matcheq
5123    // ═══════════════════════════════════════════════════════════════════
5124
5125    /// c:1315 — `rembslash("")` empty returns empty.
5126    #[test]
5127    fn rembslash_empty_returns_empty() {
5128        assert_eq!(rembslash(""), "");
5129    }
5130
5131    /// c:1315 — `rembslash` is pure.
5132    #[test]
5133    fn rembslash_is_pure() {
5134        for s in ["", "abc", r"\a", r"\\\\"] {
5135            let first = rembslash(s);
5136            for _ in 0..3 {
5137                assert_eq!(rembslash(s), first, "rembslash({:?}) must be pure", s);
5138            }
5139        }
5140    }
5141
5142    /// c:1381 — `ctokenize` is deterministic.
5143    #[test]
5144    fn ctokenize_is_deterministic() {
5145        for s in ["", "abc", "a*b", "a?b"] {
5146            let first = ctokenize(s);
5147            for _ in 0..3 {
5148                assert_eq!(
5149                    ctokenize(s),
5150                    first,
5151                    "ctokenize({:?}) must be deterministic",
5152                    s
5153                );
5154            }
5155        }
5156    }
5157
5158    /// c:1478 — `comp_quoting_string(0)` returns non-empty static.
5159    #[test]
5160    fn comp_quoting_string_zero_returns_static_str() {
5161        let _: &'static str = comp_quoting_string(0);
5162    }
5163
5164    /// c:936 — `multiquote` is pure for arbitrary input.
5165    #[test]
5166    fn multiquote_is_pure() {
5167        let _g = crate::test_util::global_state_lock();
5168        let _g2 = zle_test_setup();
5169        for s in ["", "abc", "x y"] {
5170            let first = multiquote(s, 0);
5171            for _ in 0..3 {
5172                assert_eq!(
5173                    multiquote(s, 0),
5174                    first,
5175                    "multiquote({:?}, 0) must be pure",
5176                    s
5177                );
5178            }
5179        }
5180    }
5181
5182    /// c:972 — `tildequote` is pure for non-tilde input.
5183    #[test]
5184    fn tildequote_is_pure() {
5185        let _g = crate::test_util::global_state_lock();
5186        let _g2 = zle_test_setup();
5187        for s in ["", "abc", "no_tilde", "/path/to/file"] {
5188            let first = tildequote(s, 0);
5189            for _ in 0..3 {
5190                assert_eq!(
5191                    tildequote(s, 0),
5192                    first,
5193                    "tildequote({:?}, 0) must be pure",
5194                    s
5195                );
5196            }
5197        }
5198    }
5199
5200    /// c:1338 — `remsquote(&mut empty)` returns 0.
5201    #[test]
5202    fn remsquote_empty_returns_zero_pin() {
5203        let mut s = String::new();
5204        let r = remsquote(&mut s);
5205        assert_eq!(r, 0, "empty input → 0");
5206    }
5207
5208    /// c:1014 — `check_param("")` empty returns Option<usize>.
5209    #[test]
5210    fn check_param_empty_returns_option_type() {
5211        let _g = crate::test_util::global_state_lock();
5212        let _g2 = zle_test_setup();
5213        let _: Option<usize> = check_param("", false, false);
5214    }
5215
5216    /// c:1014 — `check_param` is deterministic for empty input.
5217    #[test]
5218    fn check_param_empty_is_deterministic() {
5219        let _g = crate::test_util::global_state_lock();
5220        let _g2 = zle_test_setup();
5221        let first = check_param("", false, false);
5222        for _ in 0..3 {
5223            assert_eq!(check_param("", false, false), first);
5224        }
5225    }
5226
5227    /// c:1439 — `comp_str(false)` returns (String, i32, i32) tuple.
5228    #[test]
5229    fn comp_str_returns_string_i32_i32_tuple() {
5230        let _g = crate::test_util::global_state_lock();
5231        let _g2 = zle_test_setup();
5232        let _: (String, i32, i32) = comp_str(false);
5233    }
5234
5235    // ═══════════════════════════════════════════════════════════════════
5236    // Additional C-parity tests for Src/Zle/compcore.c
5237    // c:1505 set_comp_sep / c:1544 set_list_array / c:1555 get_user_var /
5238    // c:1645 get_data_arr / c:2681 begcmgroup / c:2735 endcmgroup /
5239    // c:2749 addexpl / c:1478 comp_quoting_string
5240    // ═══════════════════════════════════════════════════════════════════
5241
5242    /// c:1505 — `set_comp_sep` returns i32 (compile-time type pin).
5243    #[test]
5244    fn set_comp_sep_returns_i32_type() {
5245        let _g = crate::test_util::global_state_lock();
5246        let _g2 = zle_test_setup();
5247        let _: i32 = set_comp_sep();
5248    }
5249
5250    /// c:1555 — `get_user_var(None)` returns Option<Vec<String>>.
5251    #[test]
5252    fn get_user_var_returns_option_vec_string_type() {
5253        let _g = crate::test_util::global_state_lock();
5254        let _g2 = zle_test_setup();
5255        let _: Option<Vec<String>> = get_user_var(None);
5256    }
5257
5258    /// c:1555 — `get_user_var(None)` is deterministic.
5259    #[test]
5260    fn get_user_var_none_is_deterministic() {
5261        let _g = crate::test_util::global_state_lock();
5262        let _g2 = zle_test_setup();
5263        let first = get_user_var(None);
5264        for _ in 0..3 {
5265            assert_eq!(
5266                get_user_var(None),
5267                first,
5268                "get_user_var(None) must be deterministic"
5269            );
5270        }
5271    }
5272
5273    /// c:1645 — `get_data_arr("", false)` returns Option<Vec<String>>.
5274    #[test]
5275    fn get_data_arr_returns_option_vec_string_type() {
5276        let _g = crate::test_util::global_state_lock();
5277        let _g2 = zle_test_setup();
5278        let _: Option<Vec<String>> = get_data_arr("", false);
5279    }
5280
5281    /// c:1645 — `get_data_arr("", _)` empty name returns None.
5282    #[test]
5283    fn get_data_arr_empty_name_returns_none() {
5284        let _g = crate::test_util::global_state_lock();
5285        let _g2 = zle_test_setup();
5286        assert!(get_data_arr("", false).is_none(), "empty name → None");
5287        assert!(
5288            get_data_arr("", true).is_none(),
5289            "empty name (keys=true) → None"
5290        );
5291    }
5292
5293    /// c:2681 — `begcmgroup(None, 0)` safe.
5294    #[test]
5295    fn begcmgroup_none_no_panic() {
5296        let _g = crate::test_util::global_state_lock();
5297        let _g2 = zle_test_setup();
5298        begcmgroup(None, 0);
5299    }
5300
5301    /// c:2735 — `endcmgroup(None)` safe.
5302    #[test]
5303    fn endcmgroup_none_no_panic() {
5304        let _g = crate::test_util::global_state_lock();
5305        let _g2 = zle_test_setup();
5306        endcmgroup(None);
5307    }
5308
5309    /// c:2681 + c:2735 — beg/end cmgroup round-trip safe.
5310    #[test]
5311    fn begcmgroup_endcmgroup_round_trip_safe() {
5312        let _g = crate::test_util::global_state_lock();
5313        let _g2 = zle_test_setup();
5314        for _ in 0..3 {
5315            begcmgroup(None, 0);
5316            endcmgroup(None);
5317        }
5318    }
5319
5320    /// c:2749 — `addexpl(false)` safe.
5321    #[test]
5322    fn addexpl_false_no_panic() {
5323        let _g = crate::test_util::global_state_lock();
5324        let _g2 = zle_test_setup();
5325        addexpl(false);
5326    }
5327
5328    /// c:1544 — `set_list_array("", &[])` empty inputs is safe.
5329    #[test]
5330    fn set_list_array_empty_no_panic() {
5331        let _g = crate::test_util::global_state_lock();
5332        let _g2 = zle_test_setup();
5333        set_list_array("", &[]);
5334    }
5335
5336    /// c:1478 — `comp_quoting_string(N)` returns &'static str (type pin).
5337    #[test]
5338    fn comp_quoting_string_returns_static_str_type() {
5339        let _: &'static str = comp_quoting_string(0);
5340    }
5341
5342    /// c:1478 — `comp_quoting_string` is pure across stypes.
5343    #[test]
5344    fn comp_quoting_string_pure_across_stypes() {
5345        for stype in 0..10 {
5346            let first = comp_quoting_string(stype);
5347            for _ in 0..3 {
5348                assert_eq!(
5349                    comp_quoting_string(stype),
5350                    first,
5351                    "comp_quoting_string({}) must be pure",
5352                    stype
5353                );
5354            }
5355        }
5356    }
5357}