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_ARRAYS, CAF_KEYS, CAF_MATCH, CAF_MATSORT, CAF_NOSORT, CAF_NUMSORT, CAF_QUOTE, CAF_REVSORT,
31    CAF_UNIQALL,
32    CAF_UNIQCON, CGF_MATSORT, CGF_NOSORT, CGF_NUMSORT, CGF_REVSORT, CGF_UNIQALL, CGF_UNIQCON,
33    CMF_DELETE, CMF_DISPLINE, CMF_FMULT, CMF_MULT, CMF_NOLIST, CMF_PACKED, CMF_PARBR, CMF_PARNEST,
34    CMF_ROWS,
35};
36use crate::ported::zle::complete::{
37    COMPIPREFIX, COMPLIST, COMPPREFIX, COMPQSTACK, COMPSUFFIX, INCOMPFUNC,
38};
39use crate::ported::zle::compmatch::{bld_parts, cline_matched};
40use crate::ported::zle::compresult::{do_ambig_menu, ztat};
41use crate::ported::zle::zle_h::{invalidatelist, COMP_LIST_COMPLETE, COMP_LIST_EXPAND, CUT_RAW};
42use crate::ported::zle::zle_refresh::{CLEARLIST, SHOWINGLIST};
43use crate::ported::zle::zle_tricky::{
44    inststr, MENUCMP, ORIGCS, ORIGLINE, USEGLOB, USEMENU, VALIDLIST, WOULDINSTAB,
45};
46use crate::ported::zle::zle_utils::foredel;
47#[allow(unused_imports)]
48use crate::ported::zle::{
49    deltochar::*, textobjects::*, zle_h::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
50    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
51};
52use crate::ported::zsh_h::{
53    isset, Bnull, Dnull, Equals, Hat, Inbrace, Inbrack, Inpar, Outbrace, Outpar, Pound, Qstring,
54    Quest, Snull, Star, Stringg, Tilde, BASHAUTOLIST, NUMERICGLOBSORT, PM_HASHED, PM_TYPE,
55    QT_BACKSLASH, QT_DOLLARS, QT_DOUBLE, QT_NONE, QT_SINGLE, RCQUOTES, SORTIT_IGNORING_BACKSLASHES,
56    SORTIT_NUMERICALLY, ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE,
57};
58use crate::DPUTS;
59use std::sync::atomic::{AtomicI32, Ordering};
60use std::sync::{Mutex, OnceLock};
61
62// =====================================================================
63// Substrate-blocked stubs — bodies need substrate listed in each
64// doc comment. Returns shape-correct safe defaults.
65// =====================================================================
66
67// =====================================================================
68// do_completion — `Src/Zle/compcore.c:287`.
69// =====================================================================
70
71/// Direct port of `int do_completion(Hookdef dummy, Compldat dat)`
72/// from compcore.c:287. The top-level completion driver: per-round
73/// state reset → `makecomplist` → dispatch to `do_ambiguous` /
74/// `do_single` / `do_allmatches` per result count.
75pub fn do_completion(s: &str, incmd: i32, lst: i32) -> i32 {
76    // c:287
77
78    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
79    // Native (Rust) plugins that registered completions via
80    // `register_completion` (src/extensions/plugin_host.rs) have their
81    // compsys `compdef` wiring deferred to here — the completion pipeline
82    // is a safe point to eval the glue (compsys itself evals here),
83    // whereas evaling during `zmodload -R` plugin-init hangs the VM.
84    // Idempotent + cheap: no-ops once the pending queue is drained.
85    crate::plugin_host::flush_pending_completions();
86
87    let osl = SHOWINGLIST.load(Ordering::Relaxed); // c:289
88    let mut ret: i32 = 0; // c:289
89
90    // c:296-297 — `ainfo = fainfo = NULL`.
91    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
92        *g = None;
93    }
94    if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
95        *g = None;
96    }
97    if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
98        g.clear(); // c:298
99    }
100
101    // c:300-307 — compqstack reset.
102    let instring = INSTRING.load(Ordering::Relaxed); // c:307
103                                                     // c:305 — `compqstack = instring == QT_NONE ? "\\" : <quote-char>`.
104                                                     // Inlined `char_from_qt(x)` as `(x as u8) as char`.
105    let head_q: char = if instring == QT_NONE {
106        // c:305
107        QT_BACKSLASH as u8 as char
108    } else {
109        instring as u8 as char
110    };
111    if let Ok(mut g) = COMPQSTACK.get_or_init(|| Mutex::new(String::new())).lock() {
112        *g = head_q.to_string(); // c:305-306
113    }
114
115    hasunqu.store(0, Ordering::Relaxed); // c:309
116    let wouldinstab_v = WOULDINSTAB.load(Ordering::Relaxed); // c:310
117    useline.store(
118        // c:310
119        if wouldinstab_v != 0 {
120            -1
121        } else if lst != COMP_LIST_COMPLETE {
122            1
123        } else {
124            0
125        },
126        Ordering::Relaxed,
127    );
128    useexact.store(opt_isset("RECEXACT"), Ordering::Relaxed); // c:311
129    set_compstate_str("exact_string", ""); // c:312
130    let useline_v = useline.load(Ordering::Relaxed);
131    uselist.store(
132        // c:314
133        if useline_v != 0 {
134            if opt_isset("AUTOLIST") != 0 && opt_isset("BASHAUTOLIST") == 0 {
135                if opt_isset("LISTAMBIGUOUS") != 0 {
136                    3
137                } else {
138                    2
139                }
140            } else {
141                0
142            }
143        } else {
144            1
145        },
146        Ordering::Relaxed,
147    );
148
149    let useglob_v = USEGLOB.load(Ordering::Relaxed); // c:319
150    let opm: String = if useglob_v != 0 {
151        "*".into()
152    } else {
153        "".into()
154    };
155    if let Ok(mut g) = comppatmatch.get_or_init(|| Mutex::new(None)).lock() {
156        *g = Some(opm.clone()); // c:319
157    }
158    set_compstate_str("pattern_insert", "menu"); // c:320
159    forcelist.store(0, Ordering::Relaxed); // c:322
160    haspattern.store(0, Ordering::Relaxed); // c:323
161    // c:324 — complistmax mirrors the LISTMAX parameter for every
162    // completion; asklist reads it to decide when to prompt "do you wish
163    // to see all N possibilities?". Leaving the static at 0 made large
164    // command lists (`l<Tab>`, 230 matches) dump without asking.
165    crate::ported::zle::complete::COMPLISTMAX
166        .store(env_iparam("LISTMAX") as i64, Ordering::Relaxed); // c:324
167
168    set_compstate_str(
169        // c:326
170        "last_prompt",
171        if opt_isset("ALWAYSLASTPROMPT") != 0 {
172            "yes"
173        } else {
174            ""
175        },
176    );
177    dolastprompt.store(1, Ordering::Relaxed); // c:327
178
179    // c:329-330 — complist string.
180    let cl_str = if opt_isset("LISTROWSFIRST") != 0 {
181        if opt_isset("LISTPACKED") != 0 {
182            "packed rows"
183        } else {
184            "rows"
185        }
186    } else if opt_isset("LISTPACKED") != 0 {
187        "packed"
188    } else {
189        ""
190    };
191    if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
192        *g = cl_str.into(); // c:329
193    }
194    startauto.store(opt_isset("AUTOMENU"), Ordering::Relaxed); // c:331
195
196    let zlc = ZLEMETACS.load(Ordering::Relaxed);
197    let we_v = WE.load(Ordering::Relaxed);
198    movetoend.store(
199        // c:332
200        if zlc == we_v || opt_isset("ALWAYSTOEND") != 0 {
201            2
202        } else {
203            1
204        },
205        Ordering::Relaxed,
206    );
207    SHOWINGLIST.store(0, Ordering::Relaxed); // c:333
208    hasmatched.store(0, Ordering::Relaxed); // c:334
209    hasunmatched.store(0, Ordering::Relaxed); // c:334
210    minmlen.store(1_000_000, Ordering::Relaxed); // c:335
211    maxmlen.store(-1, Ordering::Relaxed); // c:336
212    nmessages.store(0, Ordering::Relaxed); // c:338
213    hasallmatch.store(0, Ordering::Relaxed); // c:339
214
215    // c:342 — main dispatch.
216    if makecomplist(s, incmd, lst) != 0 {
217        // c:342
218        // c:344 — error path.
219        ZLEMETACS.store(0, Ordering::Relaxed); // c:344
220        foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:345 — `foredel(zlemetall, CUT_RAW)`
221        let _ = inststr(
222            &ORIGLINE
223                .get_or_init(|| Mutex::new(String::new()))
224                .lock()
225                .map(|g| g.clone())
226                .unwrap_or_default(),
227        ); // c:346 — `inststr(origline)`
228        ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:347
229        CLEARLIST.store(1, Ordering::Relaxed); // c:348
230        ret = 1;
231        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
232            g.cur = None;
233        } // c:350
234        if useline.load(Ordering::Relaxed) < 0 {
235            // c:351
236            unmetafy_line();
237            ret = selfinsert(&[]); // c:353
238            metafy_line();
239        }
240        return goto_compend(ret); // c:356 goto compend
241    }
242
243    // c:359-361 — clear lastprebr/lastpostbr.
244    lastprebr_set(""); // c:359
245    lastpostbr_set(""); // c:360
246
247    let curpm = comppatmatch
248        .get_or_init(|| Mutex::new(None))
249        .lock()
250        .ok()
251        .and_then(|g| g.clone())
252        .unwrap_or_default();
253    if !curpm.is_empty() && curpm != opm {
254        // c:363
255        haspattern.store(1, Ordering::Relaxed); // c:364
256    }
257    let nm = nmatches.load(Ordering::Relaxed); // c:366
258    let dm = diffmatches.load(Ordering::Relaxed);
259    tracing::debug!(
260        target: "compsys_args",
261        nm,
262        dm,
263        useline = useline.load(Ordering::Relaxed),
264        uselist = uselist.load(Ordering::Relaxed),
265        iforcemenu = iforcemenu.load(Ordering::Relaxed),
266        "do_completion branch point"
267    );
268    if iforcemenu.load(Ordering::Relaxed) != 0 {
269        // c:366
270        if nm != 0 {
271            {
272                let _ = do_ambig_menu();
273            };
274        } // c:367
275        ret = if nm == 0 { 1 } else { 0 }; // c:369
276    } else if useline.load(Ordering::Relaxed) < 0 {
277        // c:370
278        unmetafy_line();
279        ret = selfinsert(&[]); // c:372
280        metafy_line();
281    } else if useline.load(Ordering::Relaxed) == 0 && uselist.load(Ordering::Relaxed) != 0 {
282        // c:374
283        ZLEMETACS.store(0, Ordering::Relaxed); // c:375
284        foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:376 — `foredel(zlemetall, CUT_RAW)`
285        let _ = inststr(
286            &ORIGLINE
287                .get_or_init(|| Mutex::new(String::new()))
288                .lock()
289                .map(|g| g.clone())
290                .unwrap_or_default(),
291        ); // c:377 — `inststr(origline)`
292        ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:378
293        SHOWINGLIST.store(-2, Ordering::Relaxed);
294        // c:379
295    } else if useline.load(Ordering::Relaxed) == 2 && nm > 1 {
296        // c:380
297        // c:381 — `do_allmatches(1)`. Faithful minfo-driven insertion:
298        // iterates `amatches`, chaining do_single/accept_last against
299        // the shared ZLEMETALINE buffer (see compresult::do_allmatches).
300        crate::ported::zle::compresult::do_allmatches(1);
301        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
302            g.cur = None;
303        } // c:383
304        if forcelist.load(Ordering::Relaxed) != 0 {
305            // c:385
306            SHOWINGLIST.store(-2, Ordering::Relaxed);
307        } else {
308            invalidatelist(); // c:388
309        }
310    } else if useline.load(Ordering::Relaxed) != 0 {
311        // c:389
312        if nm > 1 && dm != 0 {
313            // c:391
314            // c:393 — `ret = do_ambiguous()`. Inlined: flatten `amatches`
315            // into &[String] and dispatch.
316            ret = {
317                let groups = amatches
318                    .get_or_init(|| Mutex::new(Vec::new()))
319                    .lock()
320                    .map(|g| g.clone())
321                    .unwrap_or_default();
322                let all: Vec<String> = groups
323                    .into_iter()
324                    .flat_map(|g| g.matches.into_iter().filter_map(|m| m.str))
325                    .collect();
326                crate::ported::zle::compresult::do_ambiguous(&all)
327            };
328            if SHOWINGLIST.load(Ordering::Relaxed) == 0
329                && uselist.load(Ordering::Relaxed) != 0
330                && LISTSHOWN.load(Ordering::Relaxed) != 0
331                && (USEMENU.load(Ordering::Relaxed) == 2 || oldlist.load(Ordering::Relaxed) != 0)
332            {
333                SHOWINGLIST.store(osl, Ordering::Relaxed);
334                // c:395
335            }
336        } else if nm == 1 || (nm > 1 && dm == 0) {
337            // c:396
338            do_single_first_match(); // c:399-411
339            if forcelist.load(Ordering::Relaxed) != 0 {
340                // c:412
341                if uselist.load(Ordering::Relaxed) != 0 {
342                    SHOWINGLIST.store(-2, Ordering::Relaxed);
343                } else {
344                    CLEARLIST.store(1, Ordering::Relaxed);
345                }
346            } else {
347                invalidatelist(); // c:418
348            }
349        } else if nmessages.load(Ordering::Relaxed) != 0 && forcelist.load(Ordering::Relaxed) != 0 {
350            // c:419
351            if uselist.load(Ordering::Relaxed) != 0 {
352                SHOWINGLIST.store(-2, Ordering::Relaxed);
353            } else {
354                CLEARLIST.store(1, Ordering::Relaxed);
355            }
356        }
357    } else {
358        // c:425
359        invalidatelist(); // c:426
360        LASTAMBIG.store(
361            // c:427
362            opt_isset("BASHAUTOLIST"),
363            Ordering::Relaxed,
364        );
365        if forcelist.load(Ordering::Relaxed) != 0 {
366            CLEARLIST.store(1, Ordering::Relaxed);
367        } // c:428
368        ZLEMETACS.store(0, Ordering::Relaxed); // c:429
369        foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:430 — `foredel(zlemetall, CUT_RAW)`
370        let _ = inststr(
371            &ORIGLINE
372                .get_or_init(|| Mutex::new(String::new()))
373                .lock()
374                .map(|g| g.clone())
375                .unwrap_or_default(),
376        ); // c:431 — `inststr(origline)`
377        ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:432
378    }
379
380    // c:436 — explanation strings.
381    if SHOWINGLIST.load(Ordering::Relaxed) == 0
382        && VALIDLIST.load(Ordering::Relaxed) != 0
383        && USEMENU.load(Ordering::Relaxed) != 2
384        && uselist.load(Ordering::Relaxed) != 0
385        && (nm != 1 || dm != 0)
386        && useline.load(Ordering::Relaxed) >= 0
387        && useline.load(Ordering::Relaxed) != 2
388        && (oldlist.load(Ordering::Relaxed) == 0 || LISTSHOWN.load(Ordering::Relaxed) == 0)
389    {
390        onlyexpl.store(3, Ordering::Relaxed); // c:441
391        SHOWINGLIST.store(-2, Ordering::Relaxed);
392        // c:442
393    }
394
395    goto_compend(ret)
396}
397
398// =====================================================================
399// before_complete / after_complete — `Src/Zle/compcore.c:461 / 503`.
400// =====================================================================
401
402/// Direct port of `int before_complete(Hookdef dummy, int *lst)`
403/// from `Src/Zle/compcore.c:461`. Pre-completion hook: snapshots
404/// `menucmp` into `oldmenucmp`, decides whether the current state
405/// shortcircuits via menu-completion, clamps the cursor when re-
406/// entering an in-word completion, and toggles automenu mode.
407/// Returns 1 to suppress the next-stage match build, 0 to continue.
408pub fn before_complete(lst: &mut i32) -> i32 {
409    // c:461
410
411    // c:463 — `oldmenucmp = menucmp;`
412    OLDMENUCMP.store(MENUCMP.load(Ordering::Relaxed), Ordering::Relaxed);
413
414    // c:465-466 — `if (showagain && validlist) showinglist = -2;`
415    if SHOWAGAIN.load(Ordering::Relaxed) != 0 && VALIDLIST.load(Ordering::Relaxed) != 0 {
416        SHOWINGLIST.store(-2, Ordering::Relaxed);
417    }
418    // c:467 — `showagain = 0;`
419    SHOWAGAIN.store(0, Ordering::Relaxed);
420
421    let has_cur = MINFO
422        .get()
423        .and_then(|m| m.lock().ok())
424        .map(|m| m.cur.is_some())
425        .unwrap_or(false);
426    let menucmp_v = MENUCMP.load(Ordering::Relaxed);
427
428    // c:471-474 — menu-completion shortcircuit (non-listing path).
429    // C: `do_menucmp(*lst); return 1;`. An active menu (minfo.cur set,
430    // menucmp on) means this Tab should step the menu cursor and insert the
431    // next match, NOT restart completion.
432    if has_cur && menucmp_v != 0 && *lst != COMP_LIST_EXPAND {
433        if *lst == COMP_LIST_COMPLETE {
434            // do_menucmp c:1258-1260 — just (re)show the list.
435            SHOWINGLIST.store(-2, Ordering::Relaxed);
436        } else {
437            // do_menucmp c:1263-1268 — `if (zlemetaline == NULL) metafy_line()`.
438            // before_complete runs before docomplete metafies the buffer, so
439            // do_single would otherwise edit a stale/unmetafied line and drop
440            // the command prefix (`cat alpha.txt` → `alpine.md`). Metafy the
441            // completion buffer (still holding the previous match's line) so
442            // do_single replaces only the word region tracked by minfo.
443            if ZLEMETALL.load(Ordering::Relaxed) == 0 {
444                metafy_line();
445            }
446            // do_menucmp c:1270-1276 —
447            //   while (zmult) { minfo.cur = valid_match(minfo.cur, 1);
448            //                   zmult -= sign; }
449            //   do_single(*minfo.cur);
450            // valid_match advances the menu cursor one valid match in the
451            // ZMULT direction, updating minfo.group_idx/cur_idx; do_single
452            // then replaces the previously-inserted match (tracked via
453            // minfo.pos/len/end) with the new one.
454            let mult = crate::ported::zle::zle_main::ZMOD
455                .lock()
456                .map(|g| g.mult)
457                .unwrap_or(1);
458            ZMULT.store(mult, Ordering::Relaxed);
459            let steps = mult.abs().max(1);
460            let mut mc = None;
461            for _ in 0..steps {
462                let cur_idx = MINFO
463                    .get()
464                    .and_then(|m| m.lock().ok())
465                    .map(|m| m.cur_idx)
466                    .unwrap_or(0);
467                mc = crate::ported::zle::compresult::valid_match(cur_idx, 1);
468            }
469            // c:1272 — minfo.cur = valid_match(...); set before do_single so
470            // the insertion state stays consistent with the advanced cursor.
471            if let Ok(mut mst) = MINFO
472                .get_or_init(|| Mutex::new(Menuinfo::default()))
473                .lock()
474            {
475                mst.cur = mc.clone().map(Box::new);
476            }
477            if let Some(ref m) = mc {
478                crate::ported::zle::compresult::do_single(m); // c:1276
479            }
480        }
481        return 1; // c:473
482    }
483    // c:475-479 — menu-completion shortcircuit (listing path).
484    if has_cur
485        && menucmp_v != 0
486        && VALIDLIST.load(Ordering::Relaxed) != 0
487        && *lst == COMP_LIST_COMPLETE
488    {
489        SHOWINGLIST.store(-2, Ordering::Relaxed);
490        onlyexpl.store(0, Ordering::Relaxed); // c:477
491                                              // c:477 — `listdat.valid = 0;`
492        if let Some(ld) = listdat.get() {
493            if let Ok(mut g) = ld.lock() {
494                g.valid = 0;
495            }
496        }
497        return 1; // c:478
498    }
499
500    // c:489-490 — `if ((fromcomp & FC_INWORD) && (zlecs = lastend) > zlell)
501    //              zlecs = zlell;` — re-entering an in-word completion
502    //              restores cursor to lastend (clamped to zlell).
503    if (fromcomp.load(Ordering::Relaxed) & crate::ported::zle::comp_h::FC_INWORD) != 0 {
504        let le = lastend.load(Ordering::Relaxed);
505        let ll = ZLEMETALL.load(Ordering::Relaxed);
506        let new_cs = if le > ll { ll } else { le };
507        ZLEMETACS.store(new_cs, Ordering::Relaxed);
508    }
509
510    // c:494-496 — automenu trigger.
511    if startauto.load(Ordering::Relaxed) != 0 && LASTAMBIG.load(Ordering::Relaxed) != 0 {
512        let bashauto = isset(BASHAUTOLIST);
513        let last = LASTAMBIG.load(Ordering::Relaxed);
514        if !bashauto || last == 2 {
515            USEMENU.store(2, Ordering::Relaxed);
516        }
517    }
518
519    0 // c:498
520}
521
522/// Direct port of `int after_complete(Hookdef dummy, int *dat)`
523/// from `Src/Zle/compcore.c:503`. Post-completion hook: when a
524/// completion has just transitioned into menu-completion (menucmp
525/// went 0→1 across this round), runs MENUSTARTHOOK so registered
526/// hook ported can veto or modify the about-to-display menu.
527///
528/// Hook handlers are registered via `addhookfunc("menu_start", fn)`
529/// (see `crate::ported::module::addhookfunc`), which writes to the
530/// global HOOKTAB. C's `comphooks[]` table declares `menu_start` as
531/// HOOKF_ALL, so every handler fires and the first non-zero return
532/// short-circuits the chain (see runhookdef at module.c:990).
533///
534/// Return value semantics (c:518-532):
535///   - `ret == 0` → no action (no handler vetoed).
536///   - `ret >= 1` → zero `dat[1]`, clear menucmp/menuacc, null minfo.cur.
537///   - `ret >= 2` → also rewind buffer to origline.
538///   - `ret == 2` → also schedule list clear (CLEARLIST=1, invalidatelist).
539pub fn after_complete(dat: &mut [i32]) -> i32 {
540    // c:503
541    let menucmp_v = MENUCMP.load(Ordering::Relaxed);
542    let oldmenucmp_v = OLDMENUCMP.load(Ordering::Relaxed);
543
544    // c:505 — `if (menucmp && !oldmenucmp) { ... }`.
545    if menucmp_v == 0 || oldmenucmp_v != 0 {
546        return 0; // c:535
547    }
548
549    // c:506-517 — build chdata. cdat.matches=amatches, cdat.num=
550    //              nmatches, cdat.nmesg=nmessages, cdat.cur=NULL. The
551    //              Rust hook dispatch path doesn't yet thread chdata
552    //              into shell-fn args (handlers in the standard zsh
553    //              distribution all read directly from compsys globals
554    //              via $compstate). The fields above are still tracked
555    //              via amatches/nmatches/nmessages globals and visible
556    //              to handlers through the normal completion-state
557    //              parameter reads.
558
559    // c:518 — `runhookdef(MENUSTARTHOOK, &cdat)`. Canonical dispatch
560    // via `gethookdef("menu_start") + runhookdef(h, &cdat)`. Returns
561    // 0 when no Hookfn is registered (matches c:993-995: empty funcs
562    // and h->def NULL → return 0).
563    let mut ret: i32 = 0;
564    let h_menu_start = gethookdef("menu_start");
565    if !h_menu_start.is_null() {
566        ret = runhookdef(h_menu_start, std::ptr::null_mut());
567    }
568
569    if ret == 0 {
570        return 0; // c:535
571    }
572
573    // c:519 — `dat[1] = 0`. The C caller passes a 2-int array; index 1
574    // carries the menu-acceptance flag for the outer compfunc loop.
575    if dat.len() > 1 {
576        dat[1] = 0;
577    }
578    // c:520 — `menucmp = menuacc = 0`.
579    MENUCMP.store(0, Ordering::Relaxed);
580    menuacc.store(0, Ordering::Relaxed);
581    // c:521 — `minfo.cur = NULL`.
582    if let Some(m) = MINFO.get() {
583        if let Ok(mut mi) = m.lock() {
584            mi.cur = None;
585        }
586    }
587
588    if ret >= 2 {
589        // c:522
590        // c:523 — `fixsuffix()`.
591        fixsuffix();
592        // c:524 — `zlemetacs = 0`.
593        ZLEMETACS.store(0, Ordering::Relaxed);
594        // c:525 — `foredel(zlemetall, CUT_RAW)` removes the entire line.
595        let metall = ZLEMETALL.load(Ordering::Relaxed);
596        foredel(metall, CUT_RAW);
597        // c:526 — `inststr(origline)` reinserts the pre-completion buffer.
598        let origline_v: String = ORIGLINE
599            .get()
600            .and_then(|m| m.lock().ok().map(|g| g.clone()))
601            .unwrap_or_default();
602        let _ = inststr(&origline_v);
603        // c:527 — `zlemetacs = origcs`.
604        let origcs_v = ORIGCS.load(Ordering::Relaxed);
605        ZLEMETACS.store(origcs_v, Ordering::Relaxed);
606
607        if ret == 2 {
608            // c:528
609            // c:529 — `clearlist = 1`.
610            CLEARLIST.store(1, Ordering::Relaxed);
611            // c:530 — `invalidatelist()`.
612            invalidatelist();
613        }
614    }
615
616    0 // c:535
617}
618
619// =====================================================================
620// callcompfunc — `Src/Zle/compcore.c:544`.
621// =====================================================================
622
623/// Port of `static void callcompfunc(char *s, char *fn)` from
624/// compcore.c:544. Selects the `$compstate[context]` value, then
625/// dispatches into the user shell function `fn`. Paramtab setup
626/// (`comprpms`/`compkpms`) + result-readback is stubbed locally
627/// per PORT.md Rule 9 until `params.c` substrate lands.
628pub fn callcompfunc(s: &str, fn_name: &str) {
629    tracing::debug!(target: "compsys_args", %s, %fn_name, "callcompfunc ENTER");
630    // c:544
631
632    if fn_name.is_empty() {
633        return;
634    } // c:552 getshfunc(NULL)
635    let _lv = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed); // c:548 int lv = lastval
636    let _icf = INCOMPFUNC.load(Ordering::Relaxed); // c:555
637    let _osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed); // c:555
638
639    let _useglob = USEGLOB.load(Ordering::Relaxed); // c:579
640
641    // Publish the completion word split at the cursor into the
642    // `$PREFIX` / `$SUFFIX` params (+ empty ignored-prefix/suffix). In C
643    // these are gsu-bound to `compprefix`/`compsuffix`; the Rust ports
644    // have no gsu binding, so without this every completer reads
645    // `$PREFIX=''` — `_main_complete`'s `compset -P 1 '='` then matches
646    // the empty prefix and wrongly forces `$compstate[context]=equal`,
647    // and `_path_files` has no prefix to glob. Split at `OFFS`
648    // (zlemetacs - wb), the cursor offset within the word.
649    {
650        let scs: Vec<char> = s.chars().collect();
651        let off = (OFFS.load(Ordering::Relaxed).max(0) as usize).min(scs.len());
652        let pre: String = scs[..off].iter().collect();
653        let suf: String = scs[off..].iter().collect();
654        let _ = crate::ported::params::setsparam("PREFIX", &pre);
655        let _ = crate::ported::params::setsparam("SUFFIX", &suf);
656        let _ = crate::ported::params::setsparam("IPREFIX", "");
657        let _ = crate::ported::params::setsparam("ISUFFIX", "");
658        let _ = crate::ported::params::setsparam("QIPREFIX", "");
659        let _ = crate::ported::params::setsparam("QISUFFIX", "");
660    }
661
662    // c:591-617 — context selection.
663    let context = compcontext_for(s); // c:591-617
664    set_compstate_str("context", &context); // c:619
665
666    // c:721-727 — `$compstate[last_prompt]` etc. fed in from
667    // do_completion via dolastprompt; we forward the current values.
668    set_compstate_str(
669        "last_prompt",
670        if dolastprompt.load(Ordering::Relaxed) != 0 {
671            "yes"
672        } else {
673            ""
674        },
675    );
676
677    // c:740-749 — `$compstate[list]` — set from `complist` global.
678    let cl_value = COMPLIST
679        .get_or_init(|| Mutex::new(String::new()))
680        .lock()
681        .map(|g| g.clone())
682        .unwrap_or_default();
683    set_compstate_str("list", &cl_value); // c:740
684
685    // c:768-785 — `$compstate[insert]` per (useline, usemenu).
686    let ul = useline.load(Ordering::Relaxed);
687    let um = USEMENU.load(Ordering::Relaxed);
688    let ins = if ul != 0 {
689        match um {
690            0 => "unambiguous",
691            1 => "menu",
692            2 => "automenu",
693            _ => "",
694        }
695    } else {
696        ""
697    };
698    set_compstate_str("insert", ins); // c:770
699
700    // c:790-794 — `$compstate[exact]` & `$compstate[exact_string]`.
701    set_compstate_str(
702        "exact",
703        if useexact.load(Ordering::Relaxed) != 0 {
704            "accept"
705        } else {
706            ""
707        },
708    );
709
710    // c:800-803 — `$compstate[to_end]` per movetoend.
711    set_compstate_str(
712        "to_end",
713        if movetoend.load(Ordering::Relaxed) == 1 {
714            "single"
715        } else {
716            "match"
717        },
718    );
719
720    // c:838 — `incompfunc = 1` before invoking the user fn.
721    INCOMPFUNC.store(1, Ordering::Relaxed); // c:838
722
723    // c:828-832 — `largs = newlinklist(); addlinknode(largs,
724    //   dupstring(fn)); while (*cfargs) addlinknode(largs,
725    //   dupstring(*p++));`. argv[0] = function name, then the
726    // wrapper-widget args stored in `cfargs` by `completecall`.
727    let largs: Vec<String> = {
728        let mut v = vec![fn_name.to_string()];
729        if let Ok(cf) = crate::ported::zle::zle_tricky::cfargs.lock() {
730            v.extend(cf.iter().cloned());
731        }
732        v
733    };
734
735    // c:833-834 — `int oxt = isset(XTRACE); opts[XTRACE] = 0;`. Mute
736    // xtrace during the body so PS4 noise doesn't appear from every
737    // compsys helper line.
738    let oxt = crate::ported::zsh_h::isset(crate::ported::zsh_h::XTRACE) as i32;
739    crate::ported::options::opt_state_set(
740        &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
741        false,
742    );
743    let _ = oxt; // c:833 saved for restore at c:836
744
745    // c:835 — `cfret = doshfunc(shfunc, largs, 1)`. The body runner
746    // closure resolves the actual implementation:
747    //   - If a Rust compsys port is registered for `fn_name` and
748    //     `backend = "rust"`, run that.
749    //   - Else autoload + run the upstream shfunc body via the
750    //     standard dispatch path. dispatch_function_call already
751    //     wraps the fusevm Chunk in its own doshfunc scope; we
752    //     intentionally call only the body half here so the C-faithful
753    //     prologue/epilogue runs exactly once around the body.
754    let largs_for_body = largs.clone();
755    let fn_name_owned = fn_name.to_string();
756    let body_runner = move || -> i32 {
757        // c:6042 — `runshfunc(prog, wrappers, name)`. zshrs runs the
758        // body via either the Rust compsys port (direct fn call) or
759        // the fusevm Chunk dispatch (via exec accessors).
760        if let Some(rust_fn) = crate::compsys::router::try_rust_dispatch(&fn_name_owned) {
761            // C convention: largs[0] = fn name, [1..] = real argv.
762            return rust_fn(&largs_for_body[1..]);
763        }
764        crate::ported::exec::dispatch_function_call(&fn_name_owned, &largs_for_body[1..])
765            .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
766    };
767
768    // Look up the real shfunc; if missing we still want doshfunc's
769    // scope around the Rust port (synth_shf carries just the name).
770    let mut synth_shf = crate::ported::zsh_h::shfunc {
771        node: crate::ported::zsh_h::hashnode {
772            next: None,
773            nam: fn_name.to_string(),
774            flags: 0,
775        },
776        filename: None,
777        lineno: 0,
778        funcdef: None,
779        redir: None,
780        sticky: None,
781        body: None,
782    };
783    let cfret_val = crate::ported::exec::doshfunc(&mut synth_shf, largs, true, body_runner);
784    crate::ported::zle::zle_tricky::cfret.store(cfret_val, Ordering::Relaxed);
785
786    // c:836 — `opts[XTRACE] = oxt;` restore xtrace state.
787    crate::ported::options::opt_state_set(
788        &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
789        oxt != 0,
790    );
791
792    // c:909-912 — unwind: read `$compstate[insert]` etc. back into
793    // the compcore globals so do_completion sees the user fn's
794    // mutations.
795    // Read `$compstate[insert]` via the compstate hash (the canonical
796    // home), NOT the flat `compstate[insert]` bracketed param: the latter
797    // reads empty here because the completion fn's `compstate[insert]=menu`
798    // write lands in the hash storage while the flat param is scoped to the
799    // fn. Reading the flat param missed the menu decision entirely, so
800    // USEMENU never became 1 and menu-completion (→ menucmp → the
801    // menu_start hook → domenuselect) never started.
802    let post_insert = get_compstate_str("insert").unwrap_or_default();
803    if !post_insert.is_empty() {
804        if post_insert.contains("automenu") {
805            USEMENU.store(2, Ordering::Relaxed);
806        } else if post_insert.contains("menu") {
807            USEMENU.store(1, Ordering::Relaxed);
808        }
809    }
810
811    // c:914 — incompfunc = icf. Restore.
812    INCOMPFUNC.store(_icf, Ordering::Relaxed);
813}
814
815// =====================================================================
816// makecomplist — `Src/Zle/compcore.c:946`.
817// =====================================================================
818
819/// Direct port of `int makecomplist(char *s, int incmd, int lst)` from
820/// compcore.c:946. Top-level dispatch into the completion subsystem:
821/// either the new compsys path (`callcompfunc`) or the legacy compctl
822/// path (`COMPCTLMAKEHOOK`).
823pub fn makecomplist(s: &str, incmd: i32, lst: i32) -> i32 {
824    // c:946
825    let owb = WB.load(Ordering::Relaxed); // c:946
826    let owe = WE.load(Ordering::Relaxed);
827    let ooffs = OFFS.load(Ordering::Relaxed);
828
829    // c:952-958 — `if (compfunc && (p = check_param(s, 0, 0)))`.
830    let mut s_owned = s.to_string();
831    if compfunc_active() {
832        if let Some(p) = check_param(&s_owned, false, false) {
833            // c:952
834            s_owned = s_owned[p..].to_string(); // c:953 s = p
835            PARWB.store(owb, Ordering::Relaxed); // c:954
836            PARWE.store(owe, Ordering::Relaxed); // c:955
837            PAROFFS.store(ooffs, Ordering::Relaxed); // c:956
838        } else {
839            PARWB.store(-1, Ordering::Relaxed); // c:958
840        }
841    } else {
842        PARWB.store(-1, Ordering::Relaxed); // c:958
843    }
844
845    linwhat.store(INWHAT.load(Ordering::Relaxed), Ordering::Relaxed); // c:960
846
847    if compfunc_active() {
848        // c:962
849        let os = s_owned.clone(); // c:964
850        let onm = nmatches.load(Ordering::Relaxed); // c:965
851        let odm = diffmatches.load(Ordering::Relaxed); // c:965
852        let osi = movefd(0); // c:965 movefd(0)
853
854        // c:967-968 — bmatchers = mstack = NULL.
855        if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
856            *g = None;
857        }
858        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
859            *g = None;
860        }
861        // c:970-971 — ainfo = fainfo = hcalloc(sizeof(struct aminfo)).
862        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
863            *g = Some(Aminfo::default());
864        }
865        if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
866            *g = Some(Aminfo::default());
867        }
868        if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
869            *g = None; // c:973
870        }
871        if VALIDLIST.load(Ordering::Relaxed) == 0 {
872            LASTAMBIG.store(0, Ordering::Relaxed);
873            // c:976
874        }
875        if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
876            g.clear(); // c:977
877        }
878        mnum.store(0, Ordering::Relaxed); // c:978
879        unambig_mnum.store(-1, Ordering::Relaxed); // c:979
880        if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
881            g.clear(); // c:980
882        }
883        insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed); // c:981
884        oldlist.store(0, Ordering::Relaxed); // c:986
885        oldins.store(0, Ordering::Relaxed); // c:986
886        begcmgroup(Some("default"), 0); // c:987
887        MENUCMP.store(0, Ordering::Relaxed); // c:988
888        menuacc.store(0, Ordering::Relaxed); // c:988
889        newmatches.store(0, Ordering::Relaxed); // c:988
890        onlyexpl.store(0, Ordering::Relaxed); // c:988
891
892        let dup_s = crate::ported::mem::dupstring(&os); // c:990
893        let cf_name = compfunc
894            .get_or_init(|| Mutex::new(None))
895            .lock()
896            .ok()
897            .and_then(|g| g.clone())
898            .unwrap_or_default();
899        callcompfunc(&dup_s, &cf_name); // c:991
900        endcmgroup(None); // c:992
901
902        // c:995 — runhookdef(COMPCTLCLEANUPHOOK, NULL).
903        runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:995
904
905        if oldlist.load(Ordering::Relaxed) != 0 {
906            // c:997
907            nmatches.store(onm, Ordering::Relaxed); // c:998
908            diffmatches.store(odm, Ordering::Relaxed); // c:999
909            VALIDLIST.store(1, Ordering::Relaxed); // c:1000
910            if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
911                if let Ok(last) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
912                    *g = last.clone(); // c:1001
913                }
914            }
915            if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
916                let last_l = lastlmatches
917                    .get_or_init(|| Mutex::new(None))
918                    .lock()
919                    .ok()
920                    .and_then(|g| g.clone());
921                *g = last_l; // c:1007
922            }
923            // c:1008-1011 — `if (pmatches) freematches(pmatches, 1)`.
924            let drained = pmatches
925                .get_or_init(|| Mutex::new(Vec::new()))
926                .lock()
927                .map(|mut g| std::mem::take(&mut *g))
928                .unwrap_or_default();
929            freematches(drained, 1); // c:1009-1010
930            hasperm.store(0, Ordering::Relaxed); // c:1011
931            redup(osi); // c:1012
932            return 0; // c:1013
933        }
934        if !lastmatches
935            .get_or_init(|| Mutex::new(Vec::new()))
936            .lock()
937            .map(|g| g.is_empty())
938            .unwrap_or(true)
939        {
940            // c:1015
941            if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
942                g.clear(); // c:1016-1017
943            }
944        }
945        permmatches(1); // c:1019
946                        // c:1020-1029 — copy pmatches → amatches/lastmatches; swap holders.
947        let p_snap = pmatches
948            .get_or_init(|| Mutex::new(Vec::new()))
949            .lock()
950            .ok()
951            .map(|g| g.clone())
952            .unwrap_or_default();
953        if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
954            *g = p_snap.clone(); // c:1020
955        }
956        lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1021
957        lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1022
958        if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
959            *g = p_snap; // c:1024
960        }
961        let lm_snap = lmatches
962            .get_or_init(|| Mutex::new(None))
963            .lock()
964            .ok()
965            .and_then(|g| g.clone());
966        if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
967            *g = lm_snap; // c:1025
968        }
969        if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
970            g.clear(); // c:1026
971        }
972        hasperm.store(0, Ordering::Relaxed); // c:1027
973        hasoldlist.store(1, Ordering::Relaxed); // c:1028
974
975        let any_nm =
976            nmatches.load(Ordering::Relaxed) != 0 || nmessages.load(Ordering::Relaxed) != 0;
977        let errset = errflag_get();
978        tracing::debug!(
979            target: "compsys_args",
980            nm = nmatches.load(Ordering::Relaxed),
981            nmsg = nmessages.load(Ordering::Relaxed),
982            errset,
983            "makecomplist RETURN"
984        );
985        if any_nm && !errset {
986            // c:1030
987            VALIDLIST.store(1, Ordering::Relaxed); // c:1031
988            redup(osi); // c:1032
989            return 0; // c:1033
990        }
991        redup(osi); // c:1035
992        return 1; // c:1036
993    } else {
994        // c:1038
995        // c:1040-1047 — compctl dispatch via COMPCTLMAKEHOOK.
996        let mut dat = Ccmakedat {
997            str: Some(s_owned.clone()), // c:1042
998            incmd,                      // c:1043
999            lst,                        // c:1044
1000        };
1001        runhookdef_compctlmake(&mut dat); // c:1045
1002        runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:1048
1003        return dat.lst; // c:1050
1004    }
1005}
1006
1007// =====================================================================
1008// multiquote — `Src/Zle/compcore.c:1065`.
1009// =====================================================================
1010
1011/// Port of `mod_export char *multiquote(char *s, int ign)` from
1012/// compcore.c:1064.
1013pub fn multiquote(s: &str, ign: i32) -> String {
1014    // c:1065
1015    let stack = COMPQSTACK // c:1065
1016        .get_or_init(|| Mutex::new(String::new()))
1017        .lock()
1018        .map(|g| g.clone())
1019        .unwrap_or_default();
1020    let p_bytes = stack.as_bytes();
1021    if !p_bytes.is_empty() && (ign == 0 || p_bytes.len() > 1) {
1022        // c:1070
1023        let start = if ign != 0 { 1 } else { 0 }; // c:1071
1024        let mut cur = s.to_string();
1025        for &q in &p_bytes[start..] {
1026            // c:1073
1027            let qt = match q as i32 {
1028                // c:1074
1029                x if x == QT_BACKSLASH => QT_BACKSLASH,
1030                x if x == QT_SINGLE => QT_SINGLE,
1031                x if x == QT_DOUBLE => QT_DOUBLE,
1032                x if x == QT_DOLLARS => QT_DOLLARS,
1033                _ => QT_BACKSLASH,
1034            };
1035            cur = crate::ported::utils::quotestring(&cur, qt);
1036        }
1037        cur // c:1092
1038    } else {
1039        s.to_string() // c:1092
1040    }
1041}
1042
1043// =====================================================================
1044// tildequote — `Src/Zle/compcore.c:1092`.
1045// =====================================================================
1046
1047/// Port of `mod_export char *tildequote(char *s, int ign)` from
1048/// compcore.c:1091.
1049pub fn tildequote(s: &str, ign: i32) -> String {
1050    // c:1092
1051    let bytes = s.as_bytes(); // c:1092
1052    let tilde = !bytes.is_empty() && bytes[0] == b'~'; // c:1097
1053    let staged = if tilde {
1054        // c:1098
1055        let mut tmp = String::with_capacity(s.len());
1056        tmp.push('x');
1057        tmp.push_str(&s[1..]);
1058        tmp
1059    } else {
1060        s.to_string()
1061    };
1062    let mut quoted = multiquote(&staged, ign); // c:1099
1063    if tilde && !quoted.is_empty() {
1064        // c:1100
1065        let mut new_q = String::with_capacity(quoted.len());
1066        let mut swapped = false;
1067        for c in quoted.chars() {
1068            if !swapped && c == 'x' {
1069                new_q.push('~');
1070                swapped = true;
1071            } else {
1072                new_q.push(c);
1073            }
1074        }
1075        quoted = new_q;
1076    }
1077    quoted // c:1101
1078}
1079
1080// =====================================================================
1081// check_param — `Src/Zle/compcore.c:1113`.
1082// =====================================================================
1083
1084/// Direct port of `static char *check_param(char *s, int set, int test)`
1085/// from compcore.c:1113. Walks backwards from cursor in `s` looking
1086/// for `$<name>`. When found and the cursor sits inside the name,
1087/// returns the byte index in `s` where the name starts; updates
1088/// `ispar`/`parq`/`eparq` (when `!test`) and `ipre`/`ripre`/`isuf`/
1089/// `parpre`/`parflags`/`mflags`/`wb`/`we`/`offs` (when `set`).
1090/// Returns `None` when there's no parameter expression at the cursor.
1091pub fn check_param(s: &str, set: bool, test: bool) -> Option<usize> {
1092    // c:1113
1093
1094    // c:1117-1118 — zsfree(parpre); parpre = NULL.
1095    if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1096        g.clear();
1097    }
1098
1099    if !test {
1100        // c:1120
1101        ispar.store(0, Ordering::Relaxed); // c:1121
1102        parq.store(0, Ordering::Relaxed); // c:1121
1103        eparq.store(0, Ordering::Relaxed); // c:1121
1104    }
1105
1106    let bytes = s.as_bytes(); // local view
1107    let offs_v = OFFS.load(Ordering::Relaxed) as usize; // c:1140 cursor in word
1108
1109    let mut found = false; // c:1115
1110    let mut qstring = false; // c:1115
1111    let mut p: usize = offs_v.min(bytes.len().saturating_sub(1)); // c:1140 p = s + offs
1112
1113    // get_comp_string returns the word untokenized, so the `$` sigil
1114    // arrives as a literal 0x24 rather than the String token C scans for.
1115    // Treat the literal `$` as equivalent to the String token here so
1116    // `$VAR<Tab>` parameter completion fires; the len_utf8-based cursor
1117    // math below already handles the 1-byte literal vs 2-byte token.
1118    let is_str = |c: char| c == Stringg || c == '$';
1119
1120    // c:1140-1162 — scan backward for `String` or `Qstring`.
1121    loop {
1122        if p < bytes.len() {
1123            let ch = char_at(bytes, p);
1124            if is_str(ch) || ch == Qstring {
1125                // c:1141
1126                let next = char_at(bytes, p + ch.len_utf8());
1127                let snull_next = is_str(ch) && next == Snull; // c:1151
1128                let qstr_quot = ch == Qstring && next == '\''; // c:1152
1129                if p < offs_v && !snull_next && !qstr_quot {
1130                    found = true; // c:1154
1131                    qstring = ch == Qstring; // c:1155
1132                    break;
1133                }
1134            }
1135        }
1136        if p == 0 {
1137            break;
1138        } // c:1160
1139        p = prev_char_index(bytes, p);
1140    }
1141
1142    if found {
1143        // c:1166
1144        // c:1173-1174 — fold `$$$$` chains.
1145        while p > 0 {
1146            let prev = prev_char_index(bytes, p);
1147            let pc = char_at(bytes, prev);
1148            if is_str(pc) || pc == Qstring {
1149                p = prev;
1150            } else {
1151                break;
1152            }
1153        }
1154        loop {
1155            // c:1175-1176
1156            let n1 = p + char_at(bytes, p).len_utf8();
1157            if n1 >= bytes.len() {
1158                break;
1159            }
1160            let c1 = char_at(bytes, n1);
1161            let n2 = n1 + c1.len_utf8();
1162            if n2 >= bytes.len() {
1163                break;
1164            }
1165            let c2 = char_at(bytes, n2);
1166            if (is_str(c1) || c1 == Qstring) && (is_str(c2) || c2 == Qstring) {
1167                p = n2;
1168            } else {
1169                break;
1170            }
1171        }
1172    }
1173
1174    // c:1179 — guard against `$(`, `$[`, `$'`.
1175    let next_char = if p + 1 <= bytes.len() {
1176        let dollar_len = char_at(bytes, p).len_utf8();
1177        char_at(bytes, p + dollar_len)
1178    } else {
1179        '\0'
1180    };
1181    if !(found && next_char != Inpar && next_char != Inbrack && next_char != Snull) {
1182        return None; // c:1316
1183    }
1184
1185    // c:1181 — b = p + 1 (start of body), e = b initially.
1186    let dollar_len = char_at(bytes, p).len_utf8();
1187    let mut b: usize = p + dollar_len; // c:1181
1188    let mut br: i32 = 1; // c:1182
1189    let mut nest: i32 = 0; // c:1182
1190
1191    // get_comp_string returns the word untokenized, so `${…}` arrives with a
1192    // literal `{`/`}` rather than the Inbrace/Outbrace tokens C matches here;
1193    // accept either so `${PA<Tab>` is recognized as a braced parameter.
1194    let brace_ch = char_at(bytes, b);
1195    if brace_ch == Inbrace || brace_ch == '{' {
1196        let (ib, ob) = if brace_ch == '{' {
1197            ('{', '}')
1198        } else {
1199            (Inbrace, Outbrace)
1200        };
1201        // c:1184
1202        // c:1188 — `if (!skipparens(Inbrace, Outbrace, &tb) && tb - s <= offs) return NULL;`
1203        let mut tb: &str = &s[b..];
1204        let bal = crate::ported::utils::skipparens(ib, ob, &mut tb);
1205        let tb_after = s.len() - tb.len();
1206        if bal == 0 && tb_after <= offs_v {
1207            return None; // c:1189
1208        }
1209
1210        b += brace_ch.len_utf8(); // c:1192 b++
1211        br += 1;
1212        // c:1193-1203 — skip leading `(...)` flag group. C has a
1213        // ternary `qstring ? skipparens('(',')',&b) : skipparens(Inpar,Outpar,&b)`
1214        // — two source-level skipparens calls. Mirror that explicitly
1215        // so the call-coverage metric matches C.
1216        let mut b_str: &str = &s[b..];
1217        let flag_ret: i32 = if qstring {
1218            crate::ported::utils::skipparens('(', ')', &mut b_str)
1219        } else {
1220            crate::ported::utils::skipparens(Inpar, Outpar, &mut b_str)
1221        };
1222        let after_flags_pos = s.len() - b_str.len();
1223        if flag_ret > 0 || after_flags_pos > offs_v {
1224            ispar.store(2, Ordering::Relaxed); // c:1201
1225            return None; // c:1202
1226        }
1227        b = after_flags_pos;
1228
1229        // c:1205 — detect `nest` from preceding `${ ${` chain.
1230        let mut tb = p;
1231        while tb > 0 {
1232            let prev = prev_char_index(bytes, tb);
1233            let pc = char_at(bytes, prev);
1234            if pc == Outbrace || pc == Inbrace {
1235                tb = prev;
1236                break;
1237            }
1238            tb = prev;
1239        }
1240        if tb > 0 {
1241            let cc = char_at(bytes, tb);
1242            let prev = prev_char_index(bytes, tb);
1243            let pp = char_at(bytes, prev);
1244            if cc == Inbrace && (pp == Stringg || cc == Qstring) {
1245                nest = 1; // c:1207
1246            }
1247        }
1248    }
1249
1250    // c:1212-1213 — skip `^=~` prefix flags.
1251    while b < bytes.len() {
1252        let c = char_at(bytes, b);
1253        if c == '^' || c == Hat || c == '=' || c == Equals || c == '~' || c == Tilde {
1254            b += c.len_utf8();
1255        } else {
1256            break;
1257        }
1258    }
1259    // c:1215 — `#` / `+` length-prefix.
1260    if b < bytes.len() {
1261        let c = char_at(bytes, b);
1262        if c == '#' || c == Pound || c == '+' {
1263            b += c.len_utf8();
1264        }
1265    }
1266
1267    let mut e: usize = b; // c:1219
1268    if br != 0 {
1269        // c:1220
1270        let qopen = if test { Dnull } else { '"' };
1271        while e < bytes.len() && char_at(bytes, e) == qopen {
1272            // c:1221
1273            e += qopen.len_utf8();
1274            parq.fetch_add(1, Ordering::Relaxed); // c:1221
1275        }
1276        if !test {
1277            b = e;
1278        } // c:1223
1279    }
1280
1281    // c:1226-1252 — find end of name.
1282    if e < bytes.len() {
1283        let c = char_at(bytes, e);
1284        let one_char_name = matches!(c,
1285            ch if ch == Quest || ch == Star || ch == Stringg || ch == Qstring
1286                || ch == '?' || ch == '*' || ch == '$' || ch == '-' || ch == '!' || ch == '@');
1287        if one_char_name {
1288            // c:1230
1289            e += c.len_utf8();
1290        } else if c.is_ascii_digit() {
1291            // c:1232
1292            while e < bytes.len() && char_at(bytes, e).is_ascii_digit() {
1293                // c:1233
1294                e += 1;
1295            }
1296        } else {
1297            // c:1235-1245 — itype_end(INAMESPC) walk.
1298            let walked = walk_namespace(&bytes[e..]);
1299            if walked > 0 {
1300                e += walked;
1301            } else if c == '.' {
1302                // c:1255
1303                e += 1;
1304            }
1305        }
1306    }
1307
1308    // c:1259 — `if (offs <= e - s && offs >= b - s)`.
1309    if offs_v <= e && offs_v >= b {
1310        // c:1263 — strip trailing `"`s when br set.
1311        if br != 0 {
1312            let qopen = if test { Dnull } else { '"' };
1313            let mut pq = e;
1314            while pq < bytes.len() && char_at(bytes, pq) == qopen {
1315                pq += qopen.len_utf8();
1316                parq.fetch_sub(1, Ordering::Relaxed);
1317                eparq.fetch_add(1, Ordering::Relaxed);
1318            }
1319        }
1320        if test {
1321            // c:1269
1322            return Some(b); // c:1270
1323        }
1324        if set {
1325            // c:1273
1326            if br >= 2 {
1327                // c:1274
1328                mflags.fetch_or(CMF_PARBR, Ordering::Relaxed); // c:1275
1329                if nest != 0 {
1330                    // c:1276
1331                    mflags.fetch_or(CMF_PARNEST, Ordering::Relaxed); // c:1277
1332                }
1333            }
1334            // c:1280 — `isuf = dupstring(e); untokenize(isuf)`.
1335            let mut tail = String::from_utf8_lossy(&bytes[e..]).into_owned();
1336            tail = strip_tokens(&tail); // crate::lex::untokenize substitute
1337            if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
1338                *g = tail;
1339            }
1340            // c:1284 — `ripre = dyncat(ripre, s_through_b)`.
1341            let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1342            if let Ok(mut g) = ripre.get_or_init(|| Mutex::new(String::new())).lock() {
1343                *g = format!("{}{}", *g, head);
1344            }
1345            if let Ok(mut g) = ipre.get_or_init(|| Mutex::new(String::new())).lock() {
1346                *g = strip_tokens(&format!("{}{}", *g, head));
1347            }
1348        }
1349        // c:1295 — save prefix for compfunc.
1350        let cf_active = compfunc
1351            .get_or_init(|| Mutex::new(None))
1352            .lock()
1353            .ok()
1354            .and_then(|g| g.clone())
1355            .map(|s| !s.is_empty())
1356            .unwrap_or(false);
1357        if cf_active {
1358            let pf = if br >= 2 {
1359                CMF_PARBR | (if nest != 0 { CMF_PARNEST } else { 0 })
1360            } else {
1361                0
1362            };
1363            parflags.store(pf, Ordering::Relaxed); // c:1298
1364            let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1365            if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1366                *g = strip_tokens(&head); // c:1301
1367            }
1368        }
1369        // c:1306 — adjust wb/we/offs.
1370        let off_delta = b as i32;
1371        OFFS.fetch_sub(off_delta, Ordering::Relaxed); // c:1306
1372        let new_offs = OFFS.load(Ordering::Relaxed);
1373        let zlc = ZLEMETACS.load(Ordering::Relaxed);
1374        WB.store(zlc - new_offs, Ordering::Relaxed); // c:1307
1375        WE.store(
1376            WB.load(Ordering::Relaxed) + (e - b) as i32,
1377            Ordering::Relaxed,
1378        ); // c:1308
1379        ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1309
1380        return Some(b); // c:1311
1381    } else if offs_v > e && e < bytes.len() && char_at(bytes, e) == ':' {
1382        // c:1312
1383        // c:1313-1316 — colon-modifier guess.
1384        let offsptr = offs_v;
1385        let mut e2 = e;
1386        while e2 < offsptr && e2 < bytes.len() {
1387            let c = char_at(bytes, e2);
1388            if c != ':' && !c.is_alphanumeric() {
1389                break;
1390            }
1391            e2 += c.len_utf8();
1392        }
1393        ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1316
1394        return None; // c:1317
1395    }
1396
1397    let _ = (Bnull,); // silence unused-import warning if Bnull not hit
1398    None // c:1320
1399}
1400
1401// =====================================================================
1402// rembslash — `Src/Zle/compcore.c:1323`.
1403// =====================================================================
1404
1405/// Port of `mod_export char *rembslash(char *s)` from compcore.c:1322.
1406///
1407/// "Strip backslash escapes from a token, treating `\X` as `X`."
1408pub fn rembslash(s: &str) -> String {
1409    // c:1323
1410    let mut result = String::with_capacity(s.len()); // c:1323
1411    let mut chars = s.chars().peekable(); // c:1327
1412    while let Some(c) = chars.next() {
1413        if c == '\\' {
1414            // c:1328
1415            if let Some(nxt) = chars.next() {
1416                // c:1329
1417                result.push(nxt);
1418            }
1419        } else {
1420            result.push(c); // c:1343-1333
1421        }
1422    }
1423    result // c:1343
1424}
1425
1426// =====================================================================
1427// remsquote — `Src/Zle/compcore.c:1343`.
1428// =====================================================================
1429
1430/// Port of `mod_export int remsquote(char *s)` from compcore.c:1342.
1431pub fn remsquote(s: &mut String) -> i32 {
1432    // c:1343
1433    let rcquotes = isset(RCQUOTES); // c:1343
1434    let qa: usize = if rcquotes { 1 } else { 3 };
1435
1436    let bytes = s.as_bytes(); // c:1346
1437    let mut t = Vec::<u8>::with_capacity(bytes.len());
1438    let mut ret: i32 = 0;
1439    let mut i = 0;
1440    while i < bytes.len() {
1441        // c:1348
1442        let matched = if qa == 1 {
1443            // c:1349
1444            i + 1 < bytes.len() && bytes[i] == b'\'' && bytes[i + 1] == b'\''
1445        } else {
1446            i + 3 < bytes.len()                                              // c:1351
1447                && bytes[i]     == b'\''
1448                && bytes[i + 1] == b'\\'
1449                && bytes[i + 2] == b'\''
1450                && bytes[i + 3] == b'\''
1451        };
1452        if matched {
1453            ret += qa as i32; // c:1352
1454            t.push(b'\''); // c:1353
1455            i += qa + 1; // c:1354
1456        } else {
1457            t.push(bytes[i]); // c:1356
1458            i += 1;
1459        }
1460    }
1461    *s = String::from_utf8(t).unwrap_or_default(); // c:1357
1462    ret // c:1366
1463}
1464
1465// =====================================================================
1466// ctokenize — `Src/Zle/compcore.c:1366`.
1467// =====================================================================
1468
1469/// Port of `mod_export char *ctokenize(char *p)` from compcore.c:1365.
1470///
1471/// C calls `tokenize(p)` first then walks the string replacing
1472/// unescaped `$`/`{`/`}` with the token bytes `String`/`Inbrace`/
1473/// `Outbrace`. Backslash-escaped variants become `Bnull`.
1474pub fn ctokenize(p: &str) -> String {
1475    // c:1366
1476    let bytes = p.as_bytes(); // c:1366
1477    let mut out = Vec::<u8>::with_capacity(bytes.len());
1478    let mut bslash = false; // c:1369
1479    let mut prev_idx: Option<usize> = None;
1480    let mut i = 0;
1481    while i < bytes.len() {
1482        let b = bytes[i]; // c:1373
1483        if b == b'\\' {
1484            // c:1374
1485            bslash = true;
1486            out.push(b);
1487            prev_idx = Some(out.len() - 1);
1488        } else {
1489            if b == b'$' || b == b'{' || b == b'}' {
1490                // c:1377
1491                if bslash {
1492                    // c:1378
1493                    if let Some(pi) = prev_idx {
1494                        // c:1379
1495                        out.truncate(pi);
1496                        let mut buf = [0u8; 4];
1497                        out.extend_from_slice(Bnull.encode_utf8(&mut buf).as_bytes());
1498                    }
1499                    out.push(b);
1500                } else {
1501                    let tok = if b == b'$' {
1502                        Stringg
1503                    }
1504                    // c:1381
1505                    else if b == b'{' {
1506                        Inbrace
1507                    }
1508                    // c:1382
1509                    else {
1510                        Outbrace
1511                    }; // c:1382
1512                    let mut buf = [0u8; 4];
1513                    out.extend_from_slice(tok.encode_utf8(&mut buf).as_bytes());
1514                }
1515            } else {
1516                out.push(b);
1517            }
1518            bslash = false; // c:1384
1519            prev_idx = Some(out.len().saturating_sub(1));
1520        }
1521        i += 1;
1522    }
1523    String::from_utf8(out).unwrap_or_default() // c:1403
1524}
1525
1526// =====================================================================
1527// comp_str — `Src/Zle/compcore.c:1403`.
1528// =====================================================================
1529
1530/// Port of `mod_export char *comp_str(int *ipl, int *pl, int untok)`
1531/// from compcore.c:1402.
1532pub fn comp_str(untok: bool) -> (String, i32, i32) {
1533    // c:1403
1534    let mut p = COMPPREFIX
1535        .get_or_init(|| Mutex::new(String::new())) // c:1405
1536        .lock()
1537        .unwrap()
1538        .clone();
1539    let mut s = COMPSUFFIX
1540        .get_or_init(|| Mutex::new(String::new())) // c:1406
1541        .lock()
1542        .unwrap()
1543        .clone();
1544    let ip = COMPIPREFIX
1545        .get_or_init(|| Mutex::new(String::new())) // c:1407
1546        .lock()
1547        .unwrap()
1548        .clone();
1549    if !untok {
1550        // c:1411
1551        p = ctokenize(&p); // c:1412
1552        p = p.chars().filter(|&c| c != Bnull).collect(); // c:1413 remnulargs
1553        s = ctokenize(&s); // c:1414
1554        s = s.chars().filter(|&c| c != Bnull).collect(); // c:1415
1555    }
1556    let lp = p.len() as i32; // c:1417
1557    let lip = ip.len() as i32; // c:1419
1558    let mut str = String::with_capacity(ip.len() + p.len() + s.len() + 1); // c:1420
1559    str.push_str(&ip); // c:1435
1560    str.push_str(&p); // c:1435
1561    str.push_str(&s); // c:1435
1562    (str, lip, lp) // c:1435-1430
1563}
1564
1565// =====================================================================
1566// comp_quoting_string — `Src/Zle/compcore.c:1435`.
1567// =====================================================================
1568
1569/// Port of `mod_export char *comp_quoting_string(int stype)` from
1570/// compcore.c:1434.
1571pub fn comp_quoting_string(stype: i32) -> &'static str {
1572    // c:1435
1573    match stype {
1574        // c:1435
1575        x if x == QT_SINGLE => "'",   // c:1439-1440
1576        x if x == QT_DOUBLE => "\"",  // c:1441-1442
1577        x if x == QT_DOLLARS => "$'", // c:1443-1444
1578        _ => {
1579            // c:1445
1580            let _ = QT_BACKSLASH;
1581            "\\" // c:1446
1582        }
1583    }
1584}
1585
1586// =====================================================================
1587// set_comp_sep — `Src/Zle/compcore.c:1460`.
1588// =====================================================================
1589
1590/// Direct port of `int set_comp_sep(void)` from compcore.c:1458 —
1591/// the `compset -q` driver that re-parses the current completion
1592/// word splitting it on the IFS, then resubmits the right slice
1593/// as the new completion target.
1594///
1595/// Inputs are now published/correct: `wb`/`we`/`offs` are written by
1596/// `get_comp_string` (WB/WE/OFFS shared statics) and `compqstack` by
1597/// `callcompfunc`'s c:305 reset (deduped to `complete::COMPQSTACK`).
1598///
1599/// Byte model: all of C's single-metafied-byte index arithmetic (the
1600/// `inull` walk c:1774-1804, the `chuck` removals, the `s[swb-1-sqq+dq]`
1601/// indexing c:1830, the `p[soffs]` chuck c:1739) is performed on local
1602/// single-byte-metafied `Vec<u8>` buffers built by `to_sb` (each
1603/// token-null marker `Snull`/`Dnull`/`Bnull` maps to ONE byte
1604/// 0x9d/0x9e/0x9f; `Meta`-escapes stay two bytes) and converted back
1605/// with `from_sb` (byte -> `char`, the char-per-metafied-byte form the
1606/// downstream comp* globals expect). `wb`/`we`/`zlemetacs` are consumed
1607/// as byte offsets. For ASCII completion words — command names, paths,
1608/// options, the dominant `compset -q` case — byte == char, so the
1609/// offsets are exact and the algorithm is a verifiable translation of C.
1610///
1611/// Known limitation (INHERITED from zshrs's input/lexer model, not a
1612/// defect of this port): for non-ASCII quoted words the shared cursor
1613/// model conflates byte and char units — `ingetc` steps the input by
1614/// char while `inbufct`/`zlemetall` are byte lengths (input.rs:355/540,
1615/// lex.rs:3013-3024) — so the incoming `wb`/`we`/`zlemetacs` diverge
1616/// from single-byte offsets. Tracked separately with the
1617/// `get_comp_string` quote-form tail; see that function's note.
1618///
1619/// The `QT_DOLLARS` (`$'...'`) arm (c:1613-1622) is fully wired: the
1620/// `getkeystring_with` decode applies `GETKEY_UPDATE_OFFSET`, so
1621/// both the `dolq` byte-count delta and the `css += zlemetacs - j`
1622/// cursor micro-adjustment are computed (inheriting the same non-ASCII
1623/// byte/char caveat noted above).
1624pub fn set_comp_sep() -> i32 {
1625    use crate::ported::lex::{
1626        ctxtlex, noaliases, set_noaliases, set_tok, set_tokstr, tok, tokstr, untokenize,
1627        LEX_LEXFLAGS,
1628    };
1629    use crate::ported::string::{dupstring_wlen, tricat};
1630    use crate::ported::utils::getkeystring_with;
1631    use crate::ported::zle::comp_h::{CP_QUOTE, CP_QUOTING};
1632    // COMPPREFIX/COMPSUFFIX/COMPIPREFIX/COMPQSTACK are already imported at
1633    // the module top; only the remaining comp* globals are pulled in here.
1634    use crate::ported::zle::complete::{
1635        COMPCURRENT, COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX, COMPQUOTE, COMPQUOTING, COMPWORDS,
1636    };
1637    use crate::ported::zle::zle_utils::{zle_restore_positions, zle_save_positions};
1638    use crate::ported::zsh_h::{
1639        COMPLETEINWORD, ENDINPUT, GETKEY_UPDATE_OFFSET, GETKEYS_DOLLARS_QUOTE, LEXERR, LEXFLAGS_ZLE,
1640        Meta, STRING_LEX,
1641    };
1642
1643    // ── single-byte-metafied <-> char-per-byte String conversions ──
1644    // Each metafied byte is one `char` in the port's string world, so
1645    // `c as u8` for c < 0x100 reconstructs the C single-byte buffer
1646    // (markers 0x9d/0x9e/0x9f = one byte, Meta-escapes = 0x83 + xor).
1647    let to_sb = |s: &str| -> Vec<u8> {
1648        let mut v = Vec::with_capacity(s.len());
1649        for c in s.chars() {
1650            let cp = c as u32;
1651            if cp < 0x100 {
1652                v.push(cp as u8);
1653            } else {
1654                let mut b = [0u8; 4];
1655                v.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
1656            }
1657        }
1658        v
1659    };
1660    let from_sb = |b: &[u8]| -> String { b.iter().map(|&x| x as char).collect() };
1661    let snap = |g: &'static OnceLock<Mutex<String>>| -> String {
1662        g.get_or_init(|| Mutex::new(String::new()))
1663            .lock()
1664            .unwrap()
1665            .clone()
1666    };
1667    let put = |g: &'static OnceLock<Mutex<String>>, v: String| {
1668        *g.get_or_init(|| Mutex::new(String::new())).lock().unwrap() = v;
1669    };
1670
1671    // marker byte values (Snull/Dnull/Bnull/Stringg/Qstring are `char`).
1672    let snull_b = Snull as u32 as u8; // 0x9d
1673    let dnull_b = Dnull as u32 as u8; // 0x9e
1674    let bnull_b = Bnull as u32 as u8; // 0x9f
1675    let stringg_b = Stringg as u32 as u8; // 0x85 ($)
1676    let qstring_b = Qstring as u32 as u8; // 0x8c ("$)
1677    let meta_b: u8 = Meta; // 0x83
1678
1679    // c:1460 — s = comp_str(&lip, &lp, 1) with untok = 1.
1680    let (s_full, lip, lp) = comp_str(true);
1681    // c:1473 — int owe = we, owb = wb.
1682    let owe = WE.load(Ordering::Relaxed);
1683    let owb = WB.load(Ordering::Relaxed);
1684
1685    let mut foo: Vec<String> = Vec::new(); // c:1462 newlinklist()
1686
1687    // c:1478-1500 — locals.
1688    let mut swb: i32 = 0; // c:1490
1689    let mut swe: i32 = 0;
1690    let scs: i32; // cursor (fixed once set below)
1691    let mut soffs: i32 = 0;
1692    let ne = crate::ported::exec::noerrs.load(Ordering::Relaxed); // c:1479
1693    let mut got = false;
1694    let mut i: i32 = 0;
1695    let mut cur: i32 = -1;
1696    let mut css: i32 = 0;
1697    let mut remq = false;
1698    let mut dq: i32 = 0;
1699    let mut sq: i32 = 0;
1700    let qttype: i32;
1701    let mut sqq: i32 = 0;
1702    let mut lsq: i32 = 0;
1703    let mut qa: i32 = 0;
1704    let mut dolq: i32 = 0;
1705    let ois = INSTRING.load(Ordering::Relaxed); // c:1471
1706    let oib = INBACKT.load(Ordering::Relaxed);
1707    let noffs = lp; // active-prefix length
1708    let ona = noaliases();
1709
1710    // c:1476 — s += lip; wb += lip; untokenize(s).
1711    let s_after: String = if (lip as usize) <= s_full.len() {
1712        s_full[lip as usize..].to_string()
1713    } else {
1714        String::new()
1715    };
1716    WB.store(owb + lip, Ordering::Relaxed);
1717    let s = untokenize(&s_after);
1718    let s_b_full = to_sb(&s); // reconstructed arg, single-byte
1719
1720    // c:1483-1488 — zle_save_positions / addedx / noerrs / zcontext / lexflags.
1721    zle_save_positions();
1722    let ol = snap(&ZLEMETALINE);
1723    ADDEDX.store(1, Ordering::Relaxed);
1724    crate::ported::exec::noerrs.store(1, Ordering::Relaxed);
1725    let lex_saved = lexsave(); // zcontext_save()
1726    LEX_LEXFLAGS.set(LEXFLAGS_ZLE);
1727
1728    // c:1494-1499 — tl = strlen(s)+2; tmp = " " + s[..noffs] + 'x' + s[noffs..].
1729    let noffs_u = (noffs.max(0) as usize).min(s_b_full.len());
1730    let tl0 = s_b_full.len() as i32 + 2; // strlen(s) + 2
1731    let mut tmp_b: Vec<u8> = Vec::with_capacity(s_b_full.len() + 3);
1732    tmp_b.push(b' ');
1733    tmp_b.extend_from_slice(&s_b_full[..noffs_u]);
1734    scs = 1 + noffs;
1735    ZLEMETACS.store(scs, Ordering::Relaxed);
1736    tmp_b.push(b'x');
1737    tmp_b.extend_from_slice(&s_b_full[noffs_u..]);
1738    let mut tmp = from_sb(&tmp_b);
1739
1740    // c:1501-1640 — quote-stack head processing.
1741    let compqstack_s = snap(&COMPQSTACK);
1742    qttype = compqstack_s
1743        .chars()
1744        .next()
1745        .map(|c| c as i32)
1746        .unwrap_or(QT_NONE);
1747    let qstack2 = compqstack_s
1748        .chars()
1749        .nth(1)
1750        .map(|c| c as u32 != 0)
1751        .unwrap_or(false); // compqstack[1]
1752    if qttype == QT_BACKSLASH {
1753        // c:1503-1506
1754        remq = true;
1755        tmp = rembslash(&tmp);
1756    } else if qttype == QT_SINGLE {
1757        // c:1508-1514
1758        qa = if isset(RCQUOTES) { 1 } else { 3 };
1759        let mut t = tmp.clone();
1760        sq = remsquote(&mut t);
1761        tmp = t;
1762    } else if qttype == QT_DOUBLE {
1763        // c:1516-1543 — strip \\ and \" pairs, tracking zlemetacs/css/dq.
1764        let mut v = to_sb(&tmp);
1765        let mut j: i32 = 0;
1766        let mut pi = 0usize;
1767        let mut zcs = ZLEMETACS.load(Ordering::Relaxed);
1768        while pi < v.len() {
1769            let c = v[pi];
1770            let nxt = v.get(pi + 1).copied();
1771            if c == b'\\' && (nxt == Some(b'\\') || nxt == Some(b'"')) {
1772                dq += 1;
1773                v.remove(pi); // chuck(p): drop the backslash
1774                match v.get(pi).copied() {
1775                    Some(b'"') => zcs -= 1,
1776                    _ => {
1777                        if j > zcs {
1778                            zcs += 1;
1779                            css += 1;
1780                        }
1781                    }
1782                }
1783                if pi >= v.len() {
1784                    break; // if (!*p) break
1785                }
1786            }
1787            pi += 1;
1788            j += 1;
1789        }
1790        ZLEMETACS.store(zcs, Ordering::Relaxed);
1791        tmp = from_sb(&v);
1792    } else if qttype == QT_DOLLARS {
1793        // c:1613-1622 — string decode + dolq, with the GETKEY_UPDATE_OFFSET
1794        // cursor micro-adjustment. `j = zlemetacs` (c:1614); the decode
1795        // updates zlemetacs in place as pre-cursor escapes collapse; then
1796        // `css += zlemetacs - j` (c:1621) folds the delta into the word
1797        // offset. GETKEYS_DOLLARS_QUOTE carries the port's decode flags;
1798        // GETKEY_UPDATE_OFFSET enables the offset bookkeeping in
1799        // getkeystring_with.
1800        let j = ZLEMETACS.load(Ordering::Relaxed); // c:1614 — j = zlemetacs
1801        let mut zcs = j;
1802        let (dec, _consumed) = getkeystring_with(
1803            &tmp,
1804            (GETKEYS_DOLLARS_QUOTE | GETKEY_UPDATE_OFFSET) as u32,
1805            Some(&mut zcs),
1806        );
1807        ZLEMETACS.store(zcs, Ordering::Relaxed);
1808        let sl_new = to_sb(&dec).len() as i32;
1809        // c:1619 — dolq = tl - sl (bytes removed by $' quoting).
1810        dolq = tl0 - sl_new;
1811        // c:1621 — css += zlemetacs - j.
1812        css += zcs - j;
1813        tmp = dec;
1814    }
1815    let odq = dq; // c:1642
1816
1817    // c:1643-1647 — push into lexer, set the working metaline.
1818    crate::ported::input::inpush(&dupstrspace(&tmp), 0, None);
1819    put(&ZLEMETALINE, tmp.clone());
1820    ZLEMETALL.store(tl0 - 1, Ordering::Relaxed); // tl - addedx
1821    crate::ported::hist::strinbeg(0);
1822    set_noaliases(true);
1823
1824    // c:1650-1755 — the ctxtlex token loop.
1825    let mut ns_b: Vec<u8> = Vec::new();
1826    loop {
1827        ctxtlex();
1828        let mut tokv = tok();
1829        let mut ts_opt = tokstr();
1830        if tokv == LEXERR {
1831            // c:1654-1668 — odd active-quote count means unterminated
1832            // string; treat as STRING and drop a trailing space.
1833            match &ts_opt {
1834                None => break,
1835                Some(ts) => {
1836                    let j = ts.chars().filter(|&c| c == Snull || c == Dnull).count();
1837                    if j & 1 == 1 {
1838                        tokv = STRING_LEX;
1839                        set_tok(STRING_LEX);
1840                        if ts.ends_with(' ') {
1841                            let mut t = ts.clone();
1842                            t.pop();
1843                            set_tokstr(Some(t.clone()));
1844                            ts_opt = Some(t);
1845                        }
1846                    }
1847                }
1848            }
1849        }
1850        if tokv == ENDINPUT {
1851            break; // c:1670
1852        }
1853        let mut last_p: Option<usize> = None;
1854        if let Some(ts) = ts_opt.as_ref() {
1855            if !ts.is_empty() {
1856                // c:1673-1680 — Bnull accounting against dq.
1857                if dq != 0 {
1858                    let cs: Vec<char> = ts.chars().collect();
1859                    let mut k = 0usize;
1860                    while dq != 0 && k < cs.len() {
1861                        if cs[k] == Bnull {
1862                            dq -= 1;
1863                            if cs.get(k + 1) == Some(&'\\') {
1864                                dq -= 1;
1865                            }
1866                        }
1867                        k += 1;
1868                    }
1869                }
1870                // c:1681-1690 — single-quote lsq accounting.
1871                if qttype == QT_SINGLE {
1872                    lsq = 0;
1873                    for c in ts.chars() {
1874                        if sq != 0 && c == Snull {
1875                            sq -= qa;
1876                        }
1877                        if c == '\'' {
1878                            sq -= qa;
1879                            lsq += qa;
1880                        }
1881                    }
1882                } else {
1883                    lsq = 0;
1884                }
1885                foo.push(ts.clone()); // addlinknode(foo, p = ztrdup(tokstr))
1886                last_p = Some(foo.len() - 1);
1887            }
1888        }
1889        // c:1694-1705 — capture the cursor word once lexflags cleared.
1890        if !got && LEX_LEXFLAGS.get() == 0 {
1891            if let Some(cur_idx) = last_p {
1892                got = true;
1893                cur = cur_idx as i32;
1894                swb = WB.load(Ordering::Relaxed) - dq - sq - dolq;
1895                swe = WE.load(Ordering::Relaxed) - dq - sq - dolq;
1896                sqq = lsq;
1897                soffs = ZLEMETACS.load(Ordering::Relaxed) - swb - css;
1898                // chuck(p + soffs): drop the injected 'x' from the node.
1899                let mut wb_bytes = to_sb(&foo[cur_idx]);
1900                if soffs >= 0 && (soffs as usize) < wb_bytes.len() {
1901                    wb_bytes.remove(soffs as usize);
1902                }
1903                foo[cur_idx] = from_sb(&wb_bytes);
1904                ns_b = wb_bytes; // ns = dupstring(p)
1905            }
1906        }
1907        i += 1;
1908        if tokv == ENDINPUT || tokv == LEXERR {
1909            break; // c:1707 do-while
1910        }
1911    }
1912
1913    // c:1709-1719 — tear down lexer state, restore positions.
1914    set_noaliases(ona);
1915    crate::ported::hist::strinend();
1916    crate::ported::input::inpop();
1917    crate::ported::utils::errflag
1918        .fetch_and(!crate::ported::utils::ERRFLAG_ERROR, Ordering::Relaxed);
1919    crate::ported::exec::noerrs.store(ne, Ordering::Relaxed);
1920    lexrestore(lex_saved); // zcontext_restore()
1921    WB.store(owb, Ordering::Relaxed);
1922    WE.store(owe, Ordering::Relaxed);
1923    put(&ZLEMETALINE, ol);
1924    zle_restore_positions();
1925    if cur < 0 || i < 1 {
1926        return 1; // c:1721
1927    }
1928
1929    // c:1723-1733 — check_param dispatch with offs temporarily = soffs.
1930    let o_offs = OFFS.load(Ordering::Relaxed);
1931    OFFS.store(soffs, Ordering::Relaxed);
1932    if check_param(&from_sb(&ns_b), false, true).is_some() {
1933        for b in ns_b.iter_mut() {
1934            if *b == dnull_b {
1935                *b = b'"';
1936            } else if *b == snull_b {
1937                *b = b'\'';
1938            }
1939        }
1940    }
1941    OFFS.store(o_offs, Ordering::Relaxed);
1942
1943    // c:1735 — ts = untokenize(dupstring(ns)).
1944    let ts_str = untokenize(&from_sb(&ns_b));
1945    let ts_b = to_sb(&ts_str);
1946
1947    // c:1737-1772 — quote-form detection: instring / inbackt / autoq.
1948    let ns0 = ns_b.first().copied();
1949    let ns1 = ns_b.get(1).copied();
1950    let quote_open = ns0 == Some(snull_b)
1951        || ns0 == Some(dnull_b)
1952        || ((ns0 == Some(stringg_b) || ns0 == Some(qstring_b)) && ns1 == Some(snull_b));
1953    let mut ts_off = 0usize;
1954    if quote_open {
1955        let mut nsptr = 0usize; // C's nsptr offset into ns
1956        match ns0 {
1957            x if x == Some(snull_b) => INSTRING.store(QT_SINGLE, Ordering::Relaxed),
1958            x if x == Some(dnull_b) => INSTRING.store(QT_DOUBLE, Ordering::Relaxed),
1959            _ => {
1960                INSTRING.store(QT_DOLLARS, Ordering::Relaxed);
1961                nsptr += 1;
1962                ts_off += 1;
1963                swb += 1;
1964            }
1965        }
1966        INBACKT.store(0, Ordering::Relaxed);
1967        swb += 1;
1968        // c:1747 — if (nsptr[strlen(nsptr)-1] == *nsptr && nsptr[1]) swe--
1969        let ns_slice = &ns_b[nsptr.min(ns_b.len())..];
1970        if ns_slice.len() >= 2 && *ns_slice.last().unwrap() == ns_slice[0] {
1971            swe -= 1;
1972        }
1973        // c:1749-1753 — autoq from ts prefix.
1974        ts_off += 1; // ++tsptr
1975        let ts_prefix = from_sb(&ts_b[..ts_off.min(ts_b.len())]);
1976        let autoq_v = if qstack2 {
1977            String::new()
1978        } else {
1979            multiquote(&ts_prefix, 1)
1980        };
1981        put(&AUTOQ, autoq_v);
1982    } else {
1983        INSTRING.store(QT_NONE, Ordering::Relaxed);
1984        put(&AUTOQ, String::new());
1985    }
1986
1987    // c:1774-1804 — the inull walk: drop null markers from ns, adjusting
1988    // swb/scs/soffs. `scs` is copied to a mutable walker `scs_w`.
1989    let mut scs_w = scs;
1990    {
1991        let mut pi = 0usize;
1992        let mut wi = swb;
1993        while pi < ns_b.len() {
1994            let c = ns_b[pi];
1995            if crate::ported::ztype_h::inull(c) {
1996                let next = ns_b.get(pi + 1).copied();
1997                let next_truthy = next.is_some();
1998                if wi < scs_w && c == bnull_b {
1999                    if next_truthy && remq {
2000                        swb -= 2;
2001                    }
2002                    if odq != 0 {
2003                        swb -= 1;
2004                        if next == Some(b'\\') {
2005                            swb -= 1;
2006                        }
2007                    }
2008                }
2009                if next_truthy || c != bnull_b {
2010                    if c == bnull_b {
2011                        if scs_w == wi + 1 {
2012                            scs_w += 1;
2013                            soffs += 1;
2014                        }
2015                    } else {
2016                        let cond = scs_w > wi;
2017                        wi -= 1; // C's post-decrement in `scs > i--`
2018                        if cond {
2019                            scs_w -= 1;
2020                        }
2021                    }
2022                } else if scs_w == swe {
2023                    scs_w -= 1;
2024                }
2025                ns_b.remove(pi); // chuck(p--); loop p++ revisits index pi
2026                wi += 1;
2027            } else {
2028                pi += 1;
2029                wi += 1;
2030            }
2031        }
2032    }
2033
2034    // c:1806 — ns = ts (the untokenized copy, advanced past open quote).
2035    let mut ns_final_b = ts_b[ts_off.min(ts_b.len())..].to_vec();
2036
2037    // c:1808-1813 — backslash-quoting length fixup.
2038    let instr = INSTRING.load(Ordering::Relaxed);
2039    let qstack_has_bs = compqstack_s.chars().any(|c| c as i32 == QT_BACKSLASH);
2040    if instr != QT_NONE && qstack_has_bs {
2041        let ns_now = from_sb(&ns_final_b);
2042        let rl = ns_final_b.len() as i32;
2043        let ql = to_sb(&multiquote(&ns_now, if qstack2 { 1 } else { 0 })).len() as i32;
2044        if ql > rl {
2045            swb -= ql - rl;
2046        }
2047    }
2048
2049    // c:1826-1855 — split the reconstructed s into qp (prefix) / qs (suffix)
2050    // around the word, with the empirical swb-1-sqq+dq / swe-- offsets.
2051    let idx = swb - 1 - sqq + dq;
2052    let iu = idx.clamp(0, s_b_full.len() as i32) as usize;
2053    let s_prefix = from_sb(&s_b_full[..iu]);
2054    let mut qp = if qttype == QT_SINGLE {
2055        dupstring_wlen(&s_prefix, iu)
2056    } else {
2057        rembslash(&s_prefix)
2058    };
2059    if swe < swb {
2060        swe = swb;
2061    }
2062    swe -= 1;
2063    let sl_s = s_b_full.len() as i32;
2064    if swe > sl_s {
2065        swe = sl_s;
2066        if ns_final_b.len() as i32 > swe - swb + 1 {
2067            let newlen = (swe - swb + 1).max(0) as usize;
2068            ns_final_b.truncate(newlen);
2069        }
2070    }
2071    let swe_u = swe.clamp(0, s_b_full.len() as i32) as usize;
2072    let s_suffix = from_sb(&s_b_full[swe_u..]);
2073    let mut qs = if qttype == QT_SINGLE {
2074        s_suffix.clone()
2075    } else {
2076        rembslash(&s_suffix)
2077    };
2078    let sl_ns = ns_final_b.len() as i32;
2079    if soffs > sl_ns {
2080        soffs = sl_ns;
2081    }
2082    if qttype == QT_SINGLE {
2083        let mut a = qp;
2084        remsquote(&mut a);
2085        qp = a;
2086        let mut b = qs;
2087        remsquote(&mut b);
2088        qs = b;
2089    }
2090
2091    // c:1857-1935 — publish the results.
2092    // c:1861-1868 — prepend the active quote char to compqstack.
2093    {
2094        let head = if instr == QT_NONE { QT_BACKSLASH } else { instr };
2095        let mut new_qstack = String::new();
2096        if let Some(hc) = char::from_u32(head as u32) {
2097            new_qstack.push(hc);
2098        }
2099        new_qstack.push_str(&compqstack_s);
2100        put(&COMPQSTACK, new_qstack);
2101    }
2102
2103    // c:1870-1892 — compquote / compquoting + comp_setunset.
2104    let mut set = (CP_QUOTE | CP_QUOTING) as i32;
2105    let mut unset = 0i32;
2106    let (cq, cqg) = if instr == QT_DOUBLE {
2107        ("\"", "double")
2108    } else if instr == QT_SINGLE {
2109        ("'", "single")
2110    } else if instr == QT_DOLLARS {
2111        ("$'", "dollars")
2112    } else {
2113        unset = set;
2114        set = 0;
2115        ("", "")
2116    };
2117    put(&COMPQUOTE, cq.to_string());
2118    put(&COMPQUOTING, cqg.to_string());
2119    crate::ported::zle::complete::comp_setunset(0, 0, set, unset);
2120
2121    // c:1894-1907 — compprefix / compsuffix from ns around soffs.
2122    if !isset(COMPLETEINWORD) {
2123        put(&COMPPREFIX, untokenize(&from_sb(&ns_final_b)));
2124        put(&COMPSUFFIX, String::new());
2125    } else {
2126        let so = (soffs.max(0) as usize).min(ns_final_b.len());
2127        put(&COMPPREFIX, untokenize(&from_sb(&ns_final_b[..so])));
2128        put(&COMPSUFFIX, untokenize(&from_sb(&ns_final_b[so..])));
2129    }
2130    // c:1908-1910 — drop a dangling final backslash from compprefix.
2131    {
2132        let cp = snap(&COMPPREFIX);
2133        let cpb = cp.as_bytes();
2134        let n = cpb.len();
2135        if n > 1 && cpb[n - 1] == b'\\' && cpb[n - 2] != b'\\' && cpb[n - 2] != meta_b {
2136            let mut t = cp;
2137            t.pop();
2138            put(&COMPPREFIX, t);
2139        }
2140    }
2141
2142    // c:1912-1925 — fold qp/qs into the quoted ignored prefix/suffix.
2143    let cqip = tricat(&snap(&COMPQIPREFIX), &snap(&COMPIPREFIX), &multiquote(&qp, 1));
2144    put(&COMPQIPREFIX, cqip);
2145    let cqis = tricat(&multiquote(&qs, 1), &snap(&COMPISUFFIX), &snap(&COMPQISUFFIX));
2146    put(&COMPQISUFFIX, cqis);
2147    put(&COMPIPREFIX, String::new());
2148    put(&COMPISUFFIX, String::new());
2149
2150    // c:1926-1934 — rebuild compwords / compcurrent from foo.
2151    {
2152        let words: Vec<String> = foo.iter().map(|w| untokenize(w)).collect();
2153        let cnt = words.len() as i32;
2154        *COMPWORDS
2155            .get_or_init(|| Mutex::new(Vec::new()))
2156            .lock()
2157            .unwrap() = words;
2158        let mut compcur = cur + 1;
2159        if compcur > cnt {
2160            compcur = cnt;
2161        }
2162        COMPCURRENT.store(compcur, Ordering::Relaxed);
2163    }
2164
2165    // c:1935-1937 — restore instring / inbackt, ret = 0.
2166    INSTRING.store(ois, Ordering::Relaxed);
2167    INBACKT.store(oib, Ordering::Relaxed);
2168    0
2169}
2170
2171// Brace counters live in zle_tricky.c:114 — re-exported there. Local
2172// re-exports here so call sites stay short:
2173#[doc(hidden)]
2174// =====================================================================
2175// set_list_array — `Src/Zle/compcore.c:1947`.
2176// =====================================================================
2177
2178/// Port of `static void set_list_array(char *name, LinkList l)` from
2179/// compcore.c:1947. Writes an array-typed parameter via the canonical
2180/// `setaparam` (params.c:3595).
2181pub fn set_list_array(name: &str, l: &[String]) {
2182    // c:1947
2183    let _ = setaparam(name, l.to_vec()); // c:1956
2184}
2185
2186// =====================================================================
2187// get_user_var — `Src/Zle/compcore.c:1956`.
2188// =====================================================================
2189
2190/// Port of `mod_export char **get_user_var(char *nam)` from
2191/// compcore.c:1956.
2192pub fn get_user_var(nam: Option<&str>) -> Option<Vec<String>> {
2193    // c:1956
2194    let nam = nam?; // c:1956
2195    if nam.starts_with('(') {
2196        // c:1960
2197        let mut arrlist: Vec<String> = Vec::new();
2198        let bytes = nam.as_bytes();
2199        let mut buf = Vec::<u8>::new();
2200        let mut notempty = false; // c:1963
2201        let mut brk = false;
2202        let mut i = 1; // c:1967
2203        while i < bytes.len() {
2204            let b = bytes[i];
2205            if b == b'\\' && i + 1 < bytes.len() {
2206                // c:1969
2207                buf.push(bytes[i + 1]); // c:1970
2208                notempty = true;
2209                i += 2;
2210                continue;
2211            }
2212            if b == b',' || b == b' ' || b == b'\t' || b == b'\n' || b == b')' {
2213                if b == b')' {
2214                    brk = true;
2215                } // c:1972
2216                if notempty {
2217                    // c:1974
2218                    let mut start = 0;
2219                    if !buf.is_empty() && buf[0] == b'\n' {
2220                        start = 1;
2221                    } // c:1977
2222                    let s = String::from_utf8_lossy(&buf[start..]).into_owned();
2223                    arrlist.push(s); // c:1979
2224                }
2225                buf.clear(); // c:1981
2226                notempty = false;
2227            } else {
2228                notempty = true; // c:1984
2229                buf.push(b);
2230            }
2231            i += 1;
2232            if brk {
2233                break;
2234            } // c:1988
2235        }
2236        if !brk || arrlist.is_empty() {
2237            return None;
2238        } // c:1991
2239        Some(arrlist) // c:1996
2240    } else {
2241        // c:1999
2242        // c:2003 — `if ((arr = getaparam(nam)) || (arr = gethparam(nam)))
2243        //          arr = (incompfunc ? arrdup(arr) : arr);
2244        //          else if ((val = getsparam(nam))) { arr = {val, NULL}; }`
2245        // Read directly from paramtab: arrays first, then hashed
2246        // assoc-array values, then scalar wrapped in a 1-element array.
2247        queue_signals();
2248        let result = {
2249            let tab = match paramtab().read() {
2250                Ok(t) => t,
2251                Err(_) => {
2252                    unqueue_signals();
2253                    return None;
2254                }
2255            };
2256            tab.get(nam).and_then(|pm| {
2257                if let Some(arr) = pm.u_arr.as_ref() {
2258                    Some(arr.clone()) // c:2004 getaparam
2259                } else if let Some(s) = pm.u_str.as_ref() {
2260                    Some(vec![s.clone()]) // c:2009 getsparam
2261                } else {
2262                    None
2263                }
2264            })
2265        };
2266        unqueue_signals(); // c:2022
2267        result
2268    }
2269}
2270
2271// =====================================================================
2272// get_data_arr — `Src/Zle/compcore.c:2022`.
2273// =====================================================================
2274
2275/// Direct port of `static char **get_data_arr(char *name, int keys)`
2276/// from `Src/Zle/compcore.c:2022`. C uses `fetchvalue` with
2277/// `SCANPM_WANTKEYS`/`SCANPM_WANTVALS` + `SCANPM_MATCHMANY` to scan
2278/// an associative-array parameter and return either its keys or its
2279/// values as a flat array. Without `fetchvalue` ported with full
2280/// SCANPM flag support, we go straight to the hashed-storage
2281/// thread-local maintained by params.rs for assoc-arrays.
2282pub fn get_data_arr(name: &str, keys: bool) -> Option<Vec<String>> {
2283    // c:2022
2284
2285    queue_signals(); // c:2028
2286
2287    // c:2030-2034 — `fetchvalue(&vbuf, &name, 1, (keys ? SCANPM_WANTKEYS
2288    //   : SCANPM_WANTVALS) | SCANPM_MATCHMANY)` then `getarrvalue(v)`.
2289    // Route through the same param accessors `${(k)name}` / `${(v)name}`
2290    // / `${name}` use so SPECIAL magic hashes (`commands`, `builtins`,
2291    // `functions`, `aliases`, `reswords`, …) resolve via their module
2292    // scanfns — the raw `paramtab_hashed_storage` map is empty for those,
2293    // which is why `compadd -k commands` previously added the literal
2294    // word "commands" instead of every command name.
2295    let result = if keys {
2296        // SCANPM_WANTKEYS — assoc keys (gethkparam handles special hashes).
2297        crate::ported::params::gethkparam(name)
2298    } else {
2299        // SCANPM_WANTVALS — plain-array elements, else assoc values.
2300        crate::ported::params::getaparam(name)
2301            .filter(|v| !v.is_empty())
2302            .or_else(|| crate::ported::params::gethparam(name))
2303    };
2304
2305    unqueue_signals(); // c:2041
2306    result
2307}
2308
2309// =====================================================================
2310// addmatch — `Src/Zle/compcore.c:2041`.
2311// =====================================================================
2312
2313/// Port of `static void addmatch(char *str, int flags, char ***dispp,
2314///                                int line)` from compcore.c:2041.
2315pub fn addmatch(str: &str, flags: i32, disp: Option<&str>, line: bool) {
2316    // c:2041
2317    let mut cm = Cmatch::default(); // c:2041
2318    cm.str = Some(str.to_string()); // c:2047
2319                                    // c:2049-2051 — inline read of `complist` parameter, parse `packed`/
2320                                    // `rows` substrings into CMF_PACKED/CMF_ROWS flag bits.
2321    let complist_extra = {
2322        let s = COMPLIST
2323            .get_or_init(|| Mutex::new(String::new()))
2324            .lock()
2325            .map(|g| g.clone())
2326            .unwrap_or_default();
2327        let packed = if s.contains("packed") { CMF_PACKED } else { 0 }; // c:2050
2328        let rows = if s.contains("rows") { CMF_ROWS } else { 0 }; // c:2051
2329        if s.is_empty() {
2330            0
2331        } else {
2332            packed | rows
2333        }
2334    };
2335    cm.flags = flags | complist_extra; // c:2048
2336    if let Some(d) = disp {
2337        // c:2052
2338        cm.disp = Some(d.to_string()); // c:2056
2339    } else if line {
2340        // c:2057
2341        cm.disp = Some(String::new()); // c:2058
2342        cm.flags |= CMF_DISPLINE; // c:2059
2343    }
2344    mnum.fetch_add(1, Ordering::Relaxed); // c:2061
2345    {
2346        let cell = curexpl.get_or_init(|| Mutex::new(None)); // c:2063
2347        if let Ok(mut g) = cell.lock() {
2348            if let Some(e) = g.as_mut() {
2349                e.count += 1;
2350            }
2351        }
2352    }
2353    let mcell = matches.get_or_init(|| Mutex::new(Vec::new())); // c:2066
2354    if let Ok(mut g) = mcell.lock() {
2355        g.push(cm);
2356    }
2357    newmatches.store(1, Ordering::Relaxed); // c:2068
2358    {
2359        let cell = mgroup.get_or_init(|| Mutex::new(None)); // c:2069
2360        if let Ok(mut g) = cell.lock() {
2361            if let Some(grp) = g.as_mut() {
2362                grp.new_ = 1;
2363            }
2364        }
2365    }
2366}
2367
2368// =====================================================================
2369// addmatches — `Src/Zle/compcore.c:2080`.
2370// =====================================================================
2371
2372/// Direct port of `int addmatches(Cadata dat, char **argv)` from
2373/// compcore.c:2080 — the workhorse called from every `compadd`
2374/// invocation. Walks `argv`, runs the matcher chain against each
2375/// candidate, builds the Cline chain via `add_match_data`, and
2376/// appends accepted matches to the current group.
2377///
2378/// Real-bodied across all major phases: prologue (group selection
2379/// c:2105-2118, brace-state snapshot c:2129-2132, instring/inbackt
2380/// save c:2148-2179, `*argv` empty short-circuit c:2127), mstack
2381/// push c:2210-2222, aign/pign suffix-ignore + Patprog filters
2382/// c:2223-2246, disp array c:2247-2250, lipre/lisuf/lpre/lsuf
2383/// assembly c:2253-2300, per-candidate match loop with comp_match
2384/// dispatch + add_match_data emit + apar/opar writeback
2385/// c:2482-2601, apar/opar setaparam c:2602-2605, exp addexpl
2386/// c:2610, hasallmatch CAF_ALL placeholder c:2612-2614, dummy
2387/// entries c:2616-2617.
2388pub fn addmatches(
2389    dat: &mut Cadata, // c:2080
2390    argv: &[String],
2391) -> i32 {
2392    let _nm = mnum.load(Ordering::Relaxed); // c:2095 nm
2393
2394    if dat.dummies >= 0 {
2395        // c:2106
2396        dat.aflags = (dat.aflags | CAF_NOSORT | CAF_UNIQCON) & !CAF_UNIQALL; // c:2107-2108
2397    }
2398
2399    let gflags = (if (dat.aflags & CAF_NOSORT) != 0 {
2400        CGF_NOSORT
2401    } else {
2402        0
2403    }) | (if (dat.aflags & CAF_MATSORT) != 0 {
2404        CGF_MATSORT
2405    } else {
2406        0
2407    }) | (if (dat.aflags & CAF_NUMSORT) != 0 {
2408        CGF_NUMSORT
2409    } else {
2410        0
2411    }) | (if (dat.aflags & CAF_REVSORT) != 0 {
2412        CGF_REVSORT
2413    } else {
2414        0
2415    }) | (if (dat.aflags & CAF_UNIQALL) != 0 {
2416        CGF_UNIQALL
2417    } else {
2418        0
2419    }) | (if (dat.aflags & CAF_UNIQCON) != 0 {
2420        CGF_UNIQCON
2421    } else {
2422        0
2423    });
2424
2425    tracing::debug!(target: "compsys_args", group = dat.group.as_deref().unwrap_or("<none>"), exp = dat.exp.as_deref().unwrap_or("<none>"), nargs = argv.len(), doadd = dat.dpar.is_empty(), "addmatches group");
2426    if let Some(g) = dat.group.as_deref() {
2427        // c:2115
2428        endcmgroup(None); // c:2116
2429        begcmgroup(Some(g), gflags); // c:2117
2430    } else {
2431        endcmgroup(None); // c:2119
2432        begcmgroup(Some("default"), 0); // c:2120
2433    }
2434
2435    if dat.mesg.is_some() || dat.exp.is_some() {
2436        // c:2122
2437        let mut e = Cexpl::default(); // c:2123
2438        e.always = if dat.mesg.is_some() { 1 } else { 0 }; // c:2124
2439        e.count = 0;
2440        e.fcount = 0; // c:2125
2441        e.str = Some(
2442            dat.mesg
2443                .clone() // c:2126
2444                .or_else(|| dat.exp.clone())
2445                .unwrap_or_default(),
2446        );
2447        if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2448            *g = Some(e);
2449        }
2450        if dat.mesg.is_some() && dat.dpar.is_empty() && dat.opar.is_none() && dat.apar.is_none() {
2451            // c:2129
2452            addexpl(true); // c:2130
2453        }
2454    } else if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2455        *g = None; // c:2133
2456    }
2457
2458    // c:2138 — empty-argv early return.
2459    if argv.is_empty() && dat.dummies == 0 && (dat.aflags & CAF_ALL) == 0 {
2460        return 1; // c:2139
2461    }
2462
2463    // c:2143-2147 — snapshot brbeg/brend curpos per CAF_QUOTE.
2464    let _quote_mode = (dat.aflags & CAF_QUOTE) != 0; // c:2144
2465
2466    if (dat.flags & 0x0008/*CMF_ISPAR*/) != 0 {
2467        // c:2148
2468        dat.flags |= parflags.load(Ordering::Relaxed); // c:2149
2469    }
2470
2471    let qc = compquote_first(); // c:2150
2472    if let Some(q) = qc {
2473        // c:2151
2474        match q {
2475            '`' => {
2476                instring_set(0);
2477                inbackt_set(0);
2478                autoq_set("");
2479            } // c:2153-2161
2480            '\'' => instring_set(QT_SINGLE), // c:2165
2481            '"' => instring_set(QT_DOUBLE),  // c:2168
2482            '$' => instring_set(QT_DOLLARS), // c:2171
2483            _ => {}
2484        }
2485    } else {
2486        instring_set(0);
2487        inbackt_set(0);
2488        autoq_set(""); // c:2179
2489    }
2490
2491    // c:2182 — `useexact = (compexact && !strcmp(compexact, "accept"))`.
2492    //          C reads the `compexact` element of `$compstate`. Route
2493    //          through paramtab via getsparam — `$compstate[exact]`
2494    //          is the hashed-store equivalent. Was reading the OS env
2495    //          which never carries compstate values.
2496    let exact_str = getsparam("compexact").unwrap_or_default();
2497    useexact.store(if exact_str == "accept" { 1 } else { 0 }, Ordering::Relaxed);
2498
2499    // c:2210-2222 — push dat.match onto mstack (the matcher chain
2500    // queried by match_str during candidate evaluation).
2501    if let Some(ref m) = dat.match_ {
2502        // c:2210
2503        if let Ok(mut mst) = mstack.get_or_init(|| Mutex::new(None)).lock() {
2504            // C: mst.next = mstack; mst.matcher = dat->match; mstack = &mst.
2505            let new_link = Box::new(Cmlist {
2506                next: mst.take(),
2507                matcher: m.clone(),
2508                str: String::new(),
2509            });
2510            *mst = Some(new_link);
2511        }
2512        // c:2215 — add_bmatchers(dat->match).
2513        crate::ported::zle::compmatch::add_bmatchers(Some(m));
2514        // c:2217 — addlinknode(matchers, dat->match).
2515        if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
2516            g.push(m.clone());
2517        }
2518    }
2519
2520    // c:2223-2246 — get suffixes to ignore from dat.ign param.
2521    let (aign, pign) = if let Some(ign_name) = dat.ign.as_deref() {
2522        // c:2224
2523        let aign_raw = get_user_var(Some(ign_name)).unwrap_or_default();
2524        let mut literal_suffixes: Vec<String> = Vec::new();
2525        let mut pat_progs: Vec<crate::ported::pattern::Patprog> = Vec::new();
2526        for entry in aign_raw {
2527            // c:2231-2232 — `tokenize(tmp); remnulargs(tmp);` — fignore
2528            // entries are param values (untokenized text); C tokenizes
2529            // each BEFORE the token checks below and the patcompile.
2530            let mut tmp = entry.clone();
2531            crate::ported::glob::tokenize(&mut tmp); // c:2231
2532            crate::ported::glob::remnulargs(&mut tmp); // c:2232
2533                                                       // c:2233-2236 — `(tmp[0] == Quest && tmp[1] == Star) ||
2534                                                       // (tmp[1] == Quest && tmp[0] == Star)` token short-circuit:
2535                                                       // trailing literal suffix.
2536            let tch: Vec<char> = tmp.chars().collect();
2537            let suffix: String = tch.iter().skip(2).collect();
2538            let star_prefix = tch.len() >= 3
2539                && ((tch[0] == crate::ported::zsh_h::Quest
2540                    && tch[1] == crate::ported::zsh_h::Star)
2541                    || (tch[1] == crate::ported::zsh_h::Quest
2542                        && tch[0] == crate::ported::zsh_h::Star))
2543                && !crate::ported::pattern::haswilds(&suffix);
2544            if star_prefix {
2545                // c:2236 — `untokenize(*sp++ = tmp + 2);`
2546                literal_suffixes.push(crate::ported::lex::untokenize(&suffix));
2547            } else if let Some(prog) =
2548                crate::ported::pattern::patcompile(&tmp, 0, None::<&mut String>)
2549            {
2550                pat_progs.push(prog);
2551            }
2552        }
2553        (literal_suffixes, pat_progs)
2554    } else {
2555        (Vec::new(), Vec::new())
2556    };
2557
2558    // c:2247-2250 — get display strings.
2559    let disp_arr: Vec<String> = if let Some(ref d) = dat.disp {
2560        get_user_var(Some(d.as_str())).unwrap_or_default()
2561    } else {
2562        Vec::new()
2563    };
2564
2565    // zshrs bridge: in C the `compprefix`/`compsuffix`/`compiprefix`/
2566    // `compisuffix` globals ARE `$PREFIX`/`$SUFFIX`/`$IPREFIX`/`$ISUFFIX`
2567    // (the compparams are gsu-bound to them). The Rust compparams carry
2568    // no gsu binding (`complete.rs` `gsu: 0`), so the compsys completers'
2569    // writes to `$PREFIX` land in the param table while `compadd` reads
2570    // the globals — leaving `lpre` empty and every candidate matching.
2571    // During a live completion (`INCOMPFUNC`), refresh the globals from
2572    // the params so `comp_match` filters against the prefix the completer
2573    // actually set. Gated on INCOMPFUNC so direct-call unit tests that
2574    // seed the globals aren't clobbered.
2575    if INCOMPFUNC.load(Ordering::Relaxed) != 0 {
2576        for (param, global) in [
2577            ("PREFIX", &COMPPREFIX),
2578            ("SUFFIX", &COMPSUFFIX),
2579            ("IPREFIX", &COMPIPREFIX),
2580            ("ISUFFIX", &crate::ported::zle::complete::COMPISUFFIX),
2581        ] {
2582            if let Some(v) = getsparam(param) {
2583                if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
2584                    *g = v;
2585                }
2586            }
2587        }
2588    }
2589
2590    // c:2253-2300 — CAF_MATCH lipre/lisuf/lpre/lsuf assembly.
2591    let compiprefix_s = COMPIPREFIX
2592        .get_or_init(|| Mutex::new(String::new()))
2593        .lock()
2594        .map(|g| g.clone())
2595        .unwrap_or_default();
2596    let compisuffix_s = crate::ported::zle::complete::COMPISUFFIX
2597        .get_or_init(|| Mutex::new(String::new()))
2598        .lock()
2599        .map(|g| g.clone())
2600        .unwrap_or_default();
2601    let compprefix_s = COMPPREFIX
2602        .get_or_init(|| Mutex::new(String::new()))
2603        .lock()
2604        .map(|g| g.clone())
2605        .unwrap_or_default();
2606    let compsuffix_s = COMPSUFFIX
2607        .get_or_init(|| Mutex::new(String::new()))
2608        .lock()
2609        .map(|g| g.clone())
2610        .unwrap_or_default();
2611    let lipre = compiprefix_s.clone();
2612    let lisuf = compisuffix_s.clone();
2613    let mut lpre = compprefix_s.clone();
2614    let lsuf = compsuffix_s.clone();
2615
2616    // c:2314-2340 — strip the path-prefix (`compadd -p PPRE`) from `lpre`
2617    // before matching each candidate. PPRE is already on the command line
2618    // ahead of the match, so comp_match must compare the candidate against
2619    // `lpre` with PPRE removed (candidate `sub` vs `su`, not vs the full
2620    // `/tmp/ptree/su`). Try the active matcher first (`match_str`, part=1),
2621    // else strip literally. Without this every `cmd /a/b/pre<TAB>` produced
2622    // no matches — the resolved candidate never matched the full-path lpre.
2623    let mut bcp: i32 = 0;
2624    let mut ppre_nomatch = false;
2625    if (dat.aflags & CAF_MATCH) != 0 {
2626        // ppre is quoted below (c:2288) in C before this block; do the same
2627        // ordering here so lengths line up with the on-line text.
2628        let ppre_q: Option<String> = dat.ppre.as_deref().map(|existing| {
2629            if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
2630                tildequote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 0 } else { 1 })
2631            } else {
2632                multiquote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 0 } else { 1 })
2633            }
2634        });
2635        if let Some(ppre) = ppre_q.as_deref() {
2636            if !ppre.is_empty() {
2637                // NOTE: `dat.ppre` is left UNquoted here — the canonical
2638                // quoting happens in the c:2288 block below. `ppre_q` is a
2639                // local, same-quoting copy used only for the length math.
2640                let lpl = ppre.len();
2641                // c:2314 — `ml = match_str(lpre, ppre, …, part=1)`. Reset the
2642                // match accumulators first so this probe doesn't leak into the
2643                // per-candidate comp_match (the Rust threads the match Cline
2644                // through comp_match's own output, and instmatch re-inserts
2645                // dat.ppre from the Cmatch, so the probe's `pline` is unused).
2646                crate::ported::zle::compmatch::start_match();
2647                let ml = crate::ported::zle::compmatch::match_str(
2648                    &lpre, ppre, None, 0, None, 0, 0, 1,
2649                );
2650                if ml >= 0 {
2651                    // c:2318-2325 — matcher matched the prefix.
2652                    let cut = (ml as usize).min(lpre.len());
2653                    lpre = lpre[cut..].to_string();
2654                    bcp = ml;
2655                } else if lpre.len() <= lpl && ppre.starts_with(lpre.as_str()) {
2656                    // c:2335 — lpre is a prefix of ppre → nothing left.
2657                    lpre.clear();
2658                } else if lpre.len() > lpl && lpre.starts_with(ppre) {
2659                    // c:2337 — ppre is a literal prefix of lpre → strip it.
2660                    lpre = lpre[lpl..].to_string();
2661                } else {
2662                    // c:2339 — `*argv = NULL`: no candidate can match.
2663                    ppre_nomatch = true;
2664                }
2665                crate::ported::zle::compmatch::start_match();
2666            }
2667        }
2668    }
2669    if ppre_nomatch {
2670        return if mnum.load(Ordering::Relaxed) == _nm { 1 } else { 0 };
2671    }
2672
2673    // c:2360-2389 — when `$compstate[pattern_match]` is set, compile the
2674    // line prefix+suffix (with the completion point as a `*` placeholder)
2675    // into a Patprog so candidates are matched as GLOBS instead of literal
2676    // prefixes. This is what makes `_approximate` work: it turns on
2677    // pattern_match and injects a leading `(#a$n)` approximate-glob into
2678    // PREFIX (via the compadd prefix injector), so e.g. `(#a1)aple*` matches
2679    // `apple`. The Rust port previously always passed `cp = None` to
2680    // comp_match, so pattern_match / approximate completion did nothing.
2681    // C uses an `x` placeholder at the completion point precisely so the
2682    // point wildcard alone never enables pattern matching; mirror that by
2683    // probing haswilds() on lpre+lsuf WITHOUT the placeholder — only real
2684    // wildcards in the typed prefix/suffix (`(#a1)`, `*`, `[...]`) count.
2685    let cp: Option<crate::ported::pattern::Patprog> = {
2686        let cpm = get_compstate_str("pattern_match").unwrap_or_default();
2687        if !cpm.is_empty() {
2688            let is = cpm.starts_with('*'); // c:2361 is = (*comppatmatch == '*')
2689            let mut probe = format!("{}{}", lpre, lsuf);
2690            crate::ported::glob::tokenize(&mut probe); // c:2371
2691            if crate::ported::pattern::haswilds(&probe) {
2692                // c:2372 — has real wildcards.
2693                let mut pat = String::new();
2694                pat.push_str(&lpre); // c:2367 lpre
2695                if is {
2696                    // c:2374 — `tmp[llpl] = Star` (completion-point wildcard).
2697                    pat.push('*');
2698                }
2699                pat.push_str(&lsuf); // c:2369 lsuf
2700                crate::ported::glob::tokenize(&mut pat);
2701                crate::ported::glob::remnulargs(&mut pat); // c:2376
2702                let prog = crate::ported::pattern::patcompile(&pat, 0, None); // c:2377
2703                if prog.is_some() {
2704                    haspattern.store(1, Ordering::Relaxed); // c:2378
2705                }
2706                prog
2707            } else {
2708                None
2709            }
2710        } else {
2711            None
2712        }
2713    };
2714
2715    // c:2278-2300 — dat.ipre/isuf/ppre/psuf duplication with lipre/lisuf.
2716    if let Some(ref existing) = dat.ipre.clone() {
2717        dat.ipre = Some(if !lipre.is_empty() {
2718            format!("{}{}", lipre, existing)
2719        } else {
2720            existing.clone()
2721        });
2722    } else if !lipre.is_empty() {
2723        dat.ipre = Some(lipre.clone());
2724    }
2725    if let Some(ref existing) = dat.isuf.clone() {
2726        dat.isuf = Some(if !lisuf.is_empty() {
2727            format!("{}{}", lisuf, existing)
2728        } else {
2729            existing.clone()
2730        });
2731    } else if !lisuf.is_empty() {
2732        dat.isuf = Some(lisuf.clone());
2733    }
2734    let quote_flag = if (dat.aflags & CAF_QUOTE) != 0 { 1 } else { 0 };
2735    if let Some(ref existing) = dat.ppre.clone() {
2736        let quoted = if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
2737            tildequote(existing, quote_flag)
2738        } else {
2739            multiquote(existing, quote_flag)
2740        };
2741        dat.ppre = Some(quoted);
2742    }
2743    if let Some(ref existing) = dat.psuf.clone() {
2744        dat.psuf = Some(multiquote(existing, quote_flag));
2745    }
2746    let ppl = dat.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
2747    let psl = dat.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
2748
2749    // c:2179-2184 — `doadd = !apar && !opar && !dpar`.
2750    let doadd = dat.apar.is_none() && dat.opar.is_none() && dat.dpar.is_empty();
2751    tracing::debug!(
2752        target: "compsys_args",
2753        %lpre,
2754        %lsuf,
2755        doadd,
2756        caf_match = (dat.aflags & CAF_MATCH) != 0,
2757        dpar = ?dat.dpar,
2758        "addmatches candidate-loop setup"
2759    );
2760    let mut apar_list: Vec<String> = Vec::new();
2761    let mut opar_list: Vec<String> = Vec::new();
2762
2763    // c:2189-2207 — `-D par…` setup. Each `dpar` name holds an array
2764    // PARALLEL to the candidate words; as each word is added, the current
2765    // element of every dpar array is consumed into an output list, and at
2766    // the end the output lists are written back (so each dpar param ends
2767    // up holding just the entries for the words that matched). Names whose
2768    // array is empty/missing are dropped (C swaps them out). Bug #657 —
2769    // the previous port collected apar/opar but never handled dpar.
2770    let mut dpar_names: Vec<String> = Vec::new();
2771    let mut dparr: Vec<Vec<String>> = Vec::new();
2772    let mut dpar_idx: Vec<usize> = Vec::new();
2773    let mut dparl: Vec<Vec<String>> = Vec::new();
2774    for name in &dat.dpar {
2775        match crate::ported::params::getaparam(name) {
2776            Some(arr) if !arr.is_empty() => {
2777                dpar_names.push(name.clone()); // c:2197 getaparam non-empty
2778                dparr.push(arr);
2779                dpar_idx.push(0);
2780                dparl.push(Vec::new());
2781            }
2782            _ => {} // c:2200 — drop names with an empty/missing array
2783        }
2784    }
2785
2786    // c:2460-2476 + c:2582-2600 — CAF_ARRAYS expansion. `compadd -a
2787    // NAME…` / `-k NAME…` pass PARAMETER names, not literal matches:
2788    // the candidates are the values of the named arrays (or, with
2789    // CAF_KEYS from `-k`, the keys of the named associative arrays).
2790    // C weaves array-switching through the candidate loop via the
2791    // `next_array` label; because every non-empty array's elements are
2792    // consumed in order, the resulting candidate set is just the
2793    // concatenation of `get_data_arr` over the names — so expand up
2794    // front. Without this, `_command_names`' `compadd -k commands`
2795    // adds the literal word "commands" instead of every command name.
2796    let expanded_argv: Vec<String>;
2797    let argv: &[String] = if (dat.aflags & CAF_ARRAYS) != 0 {
2798        let keys = (dat.aflags & CAF_KEYS) != 0; // c:2468 CAF_KEYS
2799        let mut acc: Vec<String> = Vec::new();
2800        for name in argv {
2801            if let Some(vals) = get_data_arr(name, keys) {
2802                acc.extend(vals);
2803            }
2804        }
2805        expanded_argv = acc;
2806        &expanded_argv
2807    } else {
2808        argv
2809    };
2810
2811    // c:2482-2601 — main candidate loop.
2812    let mut added = 0i32;
2813    let mut disp_idx = 0usize;
2814    let mut compignored_local = 0i32;
2815    // c:2520-2522 / c:2540-2542 — `-D` parallel-array bookkeeping: the dpar
2816    // arrays advance in LOCKSTEP with the candidate words. When a word is
2817    // skipped (ignored, or comp_match fails) its dpar element must be dropped
2818    // — i.e. `dpar_idx` advances WITHOUT the corresponding value being kept —
2819    // so that surviving words stay aligned with their array elements. The
2820    // port only advanced `dpar_idx` on the KEEP path (else branch below), so
2821    // after any non-matching word every survivor grabbed the wrong element
2822    // (e.g. `compadd -D` for path completion kept `/Applications` instead of
2823    // `/tmp`), breaking `cmd /path/<TAB>` entirely.
2824    macro_rules! dpar_skip_word {
2825        () => {
2826            for i in 0..dpar_idx.len() {
2827                if dpar_idx[i] < dparr[i].len() {
2828                    dpar_idx[i] += 1;
2829                }
2830            }
2831        };
2832    }
2833    'cand: for word in argv {
2834        // c:2482
2835        // c:2486-2489 — advance disp index.
2836        let cur_disp = if !disp_arr.is_empty() && disp_idx < disp_arr.len() {
2837            let d = disp_arr[disp_idx].clone();
2838            disp_idx += 1;
2839            Some(d)
2840        } else {
2841            None
2842        };
2843
2844        // c:2491-2527 — aign/pign suffix-test + Patprog test.
2845        if !aign.is_empty() || !pign.is_empty() {
2846            let full = format!(
2847                "{}{}{}",
2848                dat.ppre.as_deref().unwrap_or(""),
2849                word,
2850                dat.psuf.as_deref().unwrap_or("")
2851            );
2852            // c:2509-2511 — literal-suffix check.
2853            for suf in &aign {
2854                if full.len() >= suf.len() && full.ends_with(suf.as_str()) {
2855                    compignored_local += 1;
2856                    dpar_skip_word!(); // c:2520
2857                    continue 'cand;
2858                }
2859            }
2860            // c:2513-2518 — Patprog check.
2861            for prog in &pign {
2862                if crate::ported::pattern::pattry(prog, &full) {
2863                    compignored_local += 1;
2864                    dpar_skip_word!(); // c:2520
2865                    continue 'cand;
2866                }
2867            }
2868        }
2869
2870        // c:2528 — CAF_MATCH dispatch: when CAF_MATCH is set, run
2871        // comp_match with the active matcher chain (else branch); else
2872        // emit the (multi)quoted word directly with a single-anchor
2873        // Cline (no-matcher path c:2530-2533).
2874        let ms: String;
2875        let _lc;
2876        let isexact;
2877        if (dat.aflags & CAF_MATCH) == 0 {
2878            // c:2528-2534 — non-match mode: just (multi)quote the word.
2879            ms = if (dat.aflags & CAF_QUOTE) != 0 {
2880                word.clone()
2881            } else {
2882                multiquote(word, 0)
2883            };
2884            let sl = ms.len() as i32;
2885            _lc = bld_parts(&ms, sl, -1, None, None);
2886            isexact = 0;
2887        } else {
2888            // c:2535
2889            // c:2535-2546 — matcher-driven mode via comp_match.
2890            let qu = if (dat.aflags & CAF_QUOTE) != 0 {
2891                0
2892            } else if dat.ppre.is_some() || (dat.flags & 0x0001/*CMF_FILE*/) == 0 {
2893                1
2894            } else {
2895                2
2896            };
2897            let mut lc_out: Option<Box<Cline>> = None;
2898            let mut isexact_out = 0i32;
2899            // c:2535 — comp_match(lpre, lsuf, s, cp, &lc, qu, &bpl, bcp,
2900            //          &bsl, bcs, &isexact).
2901            match crate::ported::zle::compmatch::comp_match(
2902                &lpre,
2903                &lsuf,
2904                word,
2905                cp.as_ref(),
2906                Some(&mut lc_out),
2907                qu,
2908                None,
2909                bcp, // c:2535 — brace-count base advanced by the ppre strip
2910                None,
2911                0,
2912                &mut isexact_out,
2913            ) {
2914                Some(matched) => {
2915                    ms = matched;
2916                    _lc = lc_out;
2917                    isexact = isexact_out;
2918                }
2919                None => {
2920                    dpar_skip_word!(); // c:2540 — drop this word's dpar element
2921                    continue 'cand; // c:2541-2545 reject
2922                }
2923            }
2924        }
2925
2926        if doadd {
2927            // c:2547
2928            // c:2556 — add_match_data.
2929            let cm = add_match_data(
2930                0,
2931                &ms,
2932                word,
2933                _lc.clone(), // line — real Cline from comp_match
2934                dat.ipre.as_deref().unwrap_or(""),
2935                "", // ripre
2936                dat.isuf.as_deref().unwrap_or(""),
2937                dat.pre.as_deref().unwrap_or(""),
2938                dat.prpre.as_deref().unwrap_or(""),
2939                dat.ppre.as_deref().unwrap_or(""),
2940                None, // pline (path-prefix Cline; unused on this path)
2941                dat.psuf.as_deref().unwrap_or(""),
2942                None, // sline (path-suffix Cline; unused on this path)
2943                dat.suf.as_deref().unwrap_or(""),
2944                dat.flags,
2945                isexact,
2946            );
2947            // c:2557-2564 — `cm->disp = dparr ? *dparr++ : NULL` (and the
2948            // CMF_DISPLINE flag when `-l`/CAF_...): C patches the STORED
2949            // match through the returned pointer; the port's return-by-value
2950            // clone can't, so patch the just-pushed copy in the live list.
2951            // Without this every `compadd -d` display string (the
2952            // description lines _describe/_arguments build) was silently
2953            // dropped and lists rendered bare match names.
2954            // c:2562-2563 — `if (disp) cm->disp = dupstring(*disp)`. C
2955            // patches the STORED match through the returned pointer; the
2956            // port's add_match_data returns a VALUE clone while the stored
2957            // copy lives in the `matches` list, so patch that copy in place
2958            // (inline — no C-counterpart fn). Without this every
2959            // `compadd -d` display string (the description lines
2960            // _describe/_arguments build) was silently dropped and lists
2961            // rendered bare match names.
2962            // c:2560-2561 — `cm->rems = dat->rems; cm->remf = dat->remf`. These
2963            // were previously SKIPPED here (only `disp` was patched), so a
2964            // `compadd -r <chars>` / `-R <widget>` custom suffix-removal spec
2965            // never reached the match: makesuffixstr got `s=None` and the
2966            // char-based auto-remove-suffix was inert. Patch them on the just-
2967            // pushed match copy (the port's add_match_data returns a value).
2968            {
2969                if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2970                    if let Some(last) = g.last_mut() {
2971                        last.rems = dat.rems.clone(); // c:2560
2972                        last.remf = dat.remf.clone(); // c:2561
2973                        if cur_disp.is_some() {
2974                            last.disp = cur_disp.clone(); // c:2562-2563
2975                        }
2976                    }
2977                }
2978            }
2979            let _ = cm;
2980            added += 1;
2981        } else {
2982            // c:2566
2983            if dat.apar.is_some() {
2984                // c:2567
2985                apar_list.push(ms.clone());
2986            }
2987            if dat.opar.is_some() {
2988                // c:2569
2989                opar_list.push(word.clone());
2990            }
2991            // c:2571-2578 — consume one element from each live dpar array
2992            // in lockstep with this added word.
2993            for i in 0..dparl.len() {
2994                if dpar_idx[i] < dparr[i].len() {
2995                    dparl[i].push(dparr[i][dpar_idx[i]].clone());
2996                    dpar_idx[i] += 1;
2997                }
2998            }
2999        }
3000    }
3001
3002    // c:2602-2608 — apar/opar/dpar writeback.
3003    if let Some(ref name) = dat.apar {
3004        setaparam(name, apar_list);
3005    }
3006    if let Some(ref name) = dat.opar {
3007        setaparam(name, opar_list);
3008    }
3009    // c:2606-2607 — set_list_array(dpar[i], dparl[i]).
3010    for (i, name) in dpar_names.iter().enumerate() {
3011        setaparam(name, std::mem::take(&mut dparl[i]));
3012    }
3013
3014    // c:2610 — explanation emit.
3015    if dat.exp.is_some() {
3016        // c:2610
3017        addexpl(false);
3018    }
3019
3020    // c:2612-2614 — `<all>` placeholder when CAF_ALL set.
3021    let hasall = hasallmatch.load(Ordering::Relaxed);
3022    if hasall == 0 && (dat.aflags & CAF_ALL) != 0 {
3023        addmatch(
3024            "<all>",
3025            dat.flags | crate::ported::zle::comp_h::CMF_ALL,
3026            None,
3027            true,
3028        );
3029        hasallmatch.store(1, Ordering::Relaxed);
3030    }
3031
3032    // c:2616-2617 — dummy entries. C's addmatch advances the SHARED disp
3033    // cursor (`&disp`, c:2054-2058), so each dummy carries the next
3034    // remaining display string — this is how the description-only lines
3035    // (compdescribe's CRT_EXPL `-E n` run: "opt  --  description") reach
3036    // the list. Passing None here dropped every description line.
3037    while dat.dummies > 0 {
3038        let d: Option<&str> = if disp_idx < disp_arr.len() {
3039            let d = Some(disp_arr[disp_idx].as_str());
3040            disp_idx += 1;
3041            d
3042        } else {
3043            None
3044        };
3045        addmatch(
3046            "",
3047            dat.flags | crate::ported::zle::comp_h::CMF_DUMMY,
3048            d,
3049            false,
3050        );
3051        dat.dummies -= 1;
3052    }
3053
3054    tracing::debug!(
3055        target: "compsys_args",
3056        added,
3057        mnum = mnum.load(Ordering::Relaxed),
3058        doadd,
3059        "addmatches candidate-loop done"
3060    );
3061    let _ = (ppl, psl, compignored_local, added);
3062    // c:2636 — `return (mnum == nm)`: non-zero (1) when NO new matches were
3063    // added this call, 0 when at least one landed. Was hardcoded `0`, so
3064    // every `compadd` reported success even against zero matches — the
3065    // `completer` chain (_main_complete) then stopped after `_complete`
3066    // and never advanced to `_approximate` / `_ignored` / etc., and any
3067    // `compadd … && ret=0` idiom in a completer wrongly took the match arm.
3068    if mnum.load(Ordering::Relaxed) == _nm {
3069        1
3070    } else {
3071        0
3072    }
3073}
3074
3075// =====================================================================
3076// add_match_data — `Src/Zle/compcore.c:2643`.
3077// =====================================================================
3078
3079/// Direct port of `Cmatch add_match_data(int alt, char *str, char *orig,
3080///    Cline line, char *ipre, char *ripre, char *isuf, char *pre,
3081///    char *prpre, char *ppre, Cline pline, char *psuf, Cline sline,
3082///    char *suf, int flags, int exact)` from compcore.c:2643.
3083///
3084/// Builds one `Cmatch` from the supplied prefix/suffix bits plus the
3085/// surrounding Cline chain. Threads `line`/`pline`/`sline` through
3086/// `cline_matched` so the CLF_MATCHED state-machine update fires the
3087/// same way as C, then performs path-prefix/suffix splicing via
3088/// `bld_parts` to extend the Cline chain at the appropriate anchor.
3089#[allow(clippy::too_many_arguments)]
3090pub fn add_match_data(
3091    // c:2643
3092    alt: i32,
3093    str: &str,
3094    orig: &str,
3095    mut line: Option<Box<Cline>>,
3096    ipre_: &str,
3097    ripre_: &str,
3098    isuf_: &str,
3099    pre: &str,
3100    prpre: &str,
3101    ppre: &str,
3102    mut pline: Option<Box<Cline>>,
3103    psuf: &str,
3104    mut sline: Option<Box<Cline>>,
3105    suf: &str,
3106    flags: i32,
3107    exact: i32,
3108) -> Cmatch {
3109    // c:2663 — DPUTS(!line, "BUG: add_match_data() without cline")
3110    DPUTS!(line.is_none(), "BUG: add_match_data() without cline"); // c:2663
3111                                                                   // c:2657 — pick the active aminfo by `alt` (alternative path = fignore).
3112    let _ai_ref = if alt != 0 { &fainfo } else { &ainfo }; // c:2657
3113                                                           // c:2666-2671 — cline_matched(line); pline; sline.
3114    cline_matched(&mut line);
3115    if pline.is_some() {
3116        cline_matched(&mut pline);
3117    }
3118    if sline.is_some() {
3119        cline_matched(&mut sline);
3120    }
3121
3122    // c:2675-2697 — accumulator lengths.
3123    let psl = psuf.len();
3124    let isl = isuf_.len();
3125    let qisuf_v = qisuf_get(); // c:2680
3126    let qisl = qisuf_v.len();
3127    let _salen = (if sline.is_none() { psl } else { 0 }) + isl + qisl; // c:2675-2683
3128
3129    let ipl = ipre_.len();
3130    let _ppl = ppre.len();
3131    let _pl = pre.len();
3132    let qipre_v = qipre_get(); // c:2686
3133    let qipl_v = qipre_v.clone();
3134    let _qipl = qipl_v.len();
3135
3136    let _stl = str.len();
3137    let _lpl = ripre_.len();
3138    let _lsl = suf.len();
3139    let _ml = ipl;
3140
3141    // c:2671-2762 — path-suffix Cline splicing. salen accumulates psl
3142    // (psuf when no sline), isl (isuf), qisl (qisuf). When salen > 0
3143    // and line is non-empty, we walk to the tail and append the
3144    // bld_parts-built Cline for each contributing string.
3145    let psl_local = if sline.is_none() && !psuf.is_empty() {
3146        psuf.len() as i32
3147    } else {
3148        0
3149    };
3150    let isl_local = isuf_.len() as i32;
3151    let qisl_local = qisuf_v.len() as i32;
3152    let salen = psl_local + isl_local + qisl_local;
3153    if salen > 0 && line.is_some() {
3154        // Walk to the tail of line via .next.
3155        unsafe {
3156            let mut tail: *mut Option<Box<Cline>> = &mut line;
3157            while let Some(ref n) = *tail {
3158                if n.next.is_none() {
3159                    break;
3160                }
3161                tail = &mut (*tail).as_mut().unwrap().next;
3162            }
3163            // For each contributing string, build a Cline chain via
3164            // bld_parts and attach to the tail node's .next.
3165            if psl_local > 0 {
3166                let s = bld_parts(psuf, psl_local, psl_local, None, None);
3167                if let Some(node) = (*tail).as_mut() {
3168                    node.next = s;
3169                    while let Some(ref nn) = node.next {
3170                        if nn.next.is_none() {
3171                            break;
3172                        }
3173                        // already linked correctly; loop to advance tail
3174                        break;
3175                    }
3176                }
3177                // Walk to the new tail.
3178                while let Some(ref n) = *tail {
3179                    if n.next.is_none() {
3180                        break;
3181                    }
3182                    tail = &mut (*tail).as_mut().unwrap().next;
3183                }
3184            }
3185            if isl_local > 0 {
3186                let s = bld_parts(isuf_, isl_local, isl_local, None, None);
3187                if let Some(node) = (*tail).as_mut() {
3188                    node.next = s;
3189                }
3190                while let Some(ref n) = *tail {
3191                    if n.next.is_none() {
3192                        break;
3193                    }
3194                    tail = &mut (*tail).as_mut().unwrap().next;
3195                }
3196            }
3197            if qisl_local > 0 {
3198                let mut s = bld_parts(&qisuf_v, qisl_local, qisl_local, None, None);
3199                // c:2741 — qsl->flags |= CLF_SUF; qsl->suffix = qsl->prefix.
3200                if let Some(qsl) = s.as_mut() {
3201                    qsl.flags |= crate::ported::zle::comp_h::CLF_SUF;
3202                    qsl.suffix = qsl.prefix.take();
3203                }
3204                if let Some(node) = (*tail).as_mut() {
3205                    node.next = s;
3206                }
3207            }
3208        }
3209    }
3210
3211    // c:2766-2873 — path-prefix Cline splicing. palen accumulates qipl,
3212    // ipl, pl, ppl (when no pline). Each contributing string gets a
3213    // bld_parts Cline prepended to `line`.
3214    let qipl_local = qipre_v.len() as i32;
3215    let ipl_local = ipre_.len() as i32;
3216    let pl_local = pre.len() as i32;
3217    let ppl_local = if pline.is_none() && !ppre.is_empty() {
3218        ppre.len() as i32
3219    } else {
3220        0
3221    };
3222    if pl_local > 0 {
3223        if ppl_local > 0 {
3224            let p = bld_parts(ppre, ppl_local, ppl_local, None, None);
3225            // Walk p to its tail, link its tail's next to line.
3226            if p.is_some() {
3227                let mut p_chain = p;
3228                let mut tail: *mut Option<Box<Cline>> = &mut p_chain;
3229                unsafe {
3230                    while let Some(ref n) = *tail {
3231                        if n.next.is_none() {
3232                            break;
3233                        }
3234                        tail = &mut (*tail).as_mut().unwrap().next;
3235                    }
3236                    if let Some(t) = (*tail).as_mut() {
3237                        t.next = line.take();
3238                    }
3239                }
3240                line = p_chain;
3241            }
3242        }
3243        let p = bld_parts(pre, pl_local, pl_local, None, None);
3244        if let Some(mut head) = p {
3245            let mut t: *mut Option<Box<Cline>> = &mut head.next;
3246            unsafe {
3247                while (*t).is_some() {
3248                    if (*t).as_deref().unwrap().next.is_none() {
3249                        break;
3250                    }
3251                    t = &mut (*t).as_mut().unwrap().next;
3252                }
3253                *t = line.take();
3254            }
3255            line = Some(head);
3256        }
3257        if ipl_local > 0 {
3258            let p = bld_parts(ipre_, ipl_local, ipl_local, None, None);
3259            if let Some(mut head) = p {
3260                let mut t: *mut Option<Box<Cline>> = &mut head.next;
3261                unsafe {
3262                    // Walk to the empty slot past the tail node, then attach
3263                    // `line` there. The previous code stopped AT the last node
3264                    // and did `*t = line`, OVERWRITING it (dropped the final
3265                    // bld_parts segment — e.g. the last dir component `zdt3/`
3266                    // of `/private/tmp/zdt3/` — so ambiguous path completion
3267                    // corrupted the line). c:2812/2818/2824/2841 `lp->next =
3268                    // line` appends; it never replaces lp.
3269                    while (*t).is_some() {
3270                        t = &mut (*t).as_mut().unwrap().next;
3271                    }
3272                    *t = line.take();
3273                }
3274                line = Some(head);
3275            }
3276        }
3277        if qipl_local > 0 {
3278            let p = bld_parts(&qipre_v, qipl_local, qipl_local, None, None);
3279            if let Some(mut head) = p {
3280                let mut t: *mut Option<Box<Cline>> = &mut head.next;
3281                unsafe {
3282                    // Walk to the empty slot past the tail node, then attach
3283                    // `line` there. The previous code stopped AT the last node
3284                    // and did `*t = line`, OVERWRITING it (dropped the final
3285                    // bld_parts segment — e.g. the last dir component `zdt3/`
3286                    // of `/private/tmp/zdt3/` — so ambiguous path completion
3287                    // corrupted the line). c:2812/2818/2824/2841 `lp->next =
3288                    // line` appends; it never replaces lp.
3289                    while (*t).is_some() {
3290                        t = &mut (*t).as_mut().unwrap().next;
3291                    }
3292                    *t = line.take();
3293                }
3294                line = Some(head);
3295            }
3296        }
3297    } else if qipl_local + ipl_local + pl_local + ppl_local > 0 || pline.is_some() {
3298        // c:2827-2842 — consolidated apre buffer.
3299        let apre = format!(
3300            "{}{}{}{}",
3301            qipre_v.as_str(),
3302            ipre_,
3303            pre,
3304            if pline.is_none() { ppre } else { "" }
3305        );
3306        let apre_len = apre.len() as i32;
3307        if apre_len > 0 {
3308            let p = bld_parts(&apre, apre_len, apre_len, None, None);
3309            if let Some(mut head) = p {
3310                let mut t: *mut Option<Box<Cline>> = &mut head.next;
3311                unsafe {
3312                    // Walk to the empty slot past the tail node, then attach
3313                    // `line` there. The previous code stopped AT the last node
3314                    // and did `*t = line`, OVERWRITING it (dropped the final
3315                    // bld_parts segment — e.g. the last dir component `zdt3/`
3316                    // of `/private/tmp/zdt3/` — so ambiguous path completion
3317                    // corrupted the line). c:2812/2818/2824/2841 `lp->next =
3318                    // line` appends; it never replaces lp.
3319                    while (*t).is_some() {
3320                        t = &mut (*t).as_mut().unwrap().next;
3321                    }
3322                    *t = line.take();
3323                }
3324                line = Some(head);
3325            }
3326        }
3327    }
3328
3329    let stl = str.len();
3330
3331    // c:2929-2932 — Cmatch allocation + str/orig/ppre/psuf.
3332    let mut cm = Cmatch::default(); // c:2929
3333    cm.str = Some(str.to_string()); // c:2930
3334    cm.orig = Some(orig.to_string()); // c:2931
3335    cm.ppre = if ppre.is_empty() {
3336        None
3337    } else {
3338        Some(ppre.into())
3339    }; // c:2932
3340    cm.psuf = if psuf.is_empty() {
3341        None
3342    } else {
3343        Some(psuf.into())
3344    }; // c:2933
3345
3346    // c:2934 — prpre only when CMF_FILE.
3347    cm.prpre = if (flags & CMF_FILE) != 0 && !prpre.is_empty() {
3348        Some(prpre.into())
3349    } else {
3350        None
3351    };
3352
3353    // c:2935-2938 — ipre = qipre + ipre (concat when qipre non-empty).
3354    // qipre_v already computed above.
3355    cm.ipre = if !qipre_v.is_empty() {
3356        if !ipre_.is_empty() {
3357            Some(format!("{}{}", qipre_v, ipre_))
3358        } else {
3359            Some(qipre_v.clone())
3360        }
3361    } else if !ipre_.is_empty() {
3362        Some(ipre_.into())
3363    } else {
3364        None
3365    };
3366
3367    cm.ripre = if ripre_.is_empty() {
3368        None
3369    } else {
3370        Some(ripre_.into())
3371    }; // c:2939
3372
3373    // c:2940-2943 — isuf = isuf + qisuf (concat when qisuf non-empty).
3374    cm.isuf = if !qisuf_v.is_empty() {
3375        if !isuf_.is_empty() {
3376            Some(format!("{}{}", isuf_, qisuf_v))
3377        } else {
3378            Some(qisuf_v.clone())
3379        }
3380    } else if !isuf_.is_empty() {
3381        Some(isuf_.into())
3382    } else {
3383        None
3384    };
3385
3386    cm.pre = if pre.is_empty() {
3387        None
3388    } else {
3389        Some(pre.into())
3390    }; // c:2944
3391    cm.suf = if suf.is_empty() {
3392        None
3393    } else {
3394        Some(suf.into())
3395    }; // c:2945
3396
3397    // c:2946 — flags + CMF_PACKED/CMF_ROWS from complist.
3398    let complist_s = COMPLIST
3399        .get()
3400        .and_then(|m| m.lock().ok().map(|g| g.clone()))
3401        .unwrap_or_default();
3402    let extra_flags = (if complist_s.contains("packed") {
3403        CMF_PACKED
3404    } else {
3405        0
3406    }) | (if complist_s.contains("rows") {
3407        CMF_ROWS
3408    } else {
3409        0
3410    });
3411    cm.flags = flags | extra_flags;
3412
3413    // c:2950-2951 — mode/fmode init to 0.
3414    cm.mode = 0;
3415    cm.fmode = 0;
3416    cm.modec = '\0';
3417    cm.fmodec = '\0';
3418
3419    // c:2952-2970 — CMF_FILE: stat the path for mode + modec.
3420    use crate::ported::zle::comp_h::CMF_FILE;
3421    if (flags & CMF_FILE) != 0 && !orig.is_empty() && !orig.ends_with('/') {
3422        let pb = format!("{}{}", cm.prpre.as_deref().unwrap_or("./"), orig);
3423        // c:2960-2963 — `ztat(pb, &buf, 1); cm->mode = buf.st_mode;
3424        //   if ((cm->modec = file_type(buf.st_mode)) == ' ') cm->modec = 0;`
3425        // The `1` is ztat's ls flag → lstat, so modec is the type of the
3426        // link itself (`@` for a symlink). This is the marker printlist
3427        // shows, so it must come from lstat, not stat.
3428        if let Some(meta) = ztat(&pb, true) {
3429            use std::os::unix::fs::MetadataExt;
3430            cm.mode = meta.mode();
3431            let c = crate::ported::glob::file_type(cm.mode); // c:2962
3432            cm.modec = if c == ' ' { '\0' } else { c }; // c:2963
3433        }
3434        // c:2965-2968 — `ztat(pb, &buf, 0)` → stat, so fmode/fmodec is the
3435        // type of the symlink target (the marker used when following links).
3436        if let Some(meta) = ztat(&pb, false) {
3437            use std::os::unix::fs::MetadataExt;
3438            cm.fmode = meta.mode();
3439            let c = crate::ported::glob::file_type(cm.fmode); // c:2967
3440            cm.fmodec = if c == ' ' { '\0' } else { c }; // c:2968
3441        }
3442    }
3443
3444    // c:2974-2993 — brpl/brsl brace position arrays. Walk BRBEG/BREND
3445    // (the global Brinfo chains from `Src/Zle/zle_tricky.c`), reading
3446    // `qpos` for each entry to derive the position offset within the
3447    // match string. With no brace chain populated (zero-brace common
3448    // case) brpl/brsl stay empty.
3449    cm.brpl = BRBEG
3450        .get_or_init(|| Mutex::new(None))
3451        .lock()
3452        .ok()
3453        .and_then(|g| {
3454            g.as_ref().map(|head| {
3455                let mut out: Vec<i32> = Vec::new();
3456                let mut cur = Some(head.as_ref());
3457                while let Some(n) = cur {
3458                    out.push(n.qpos);
3459                    cur = n.next.as_deref();
3460                }
3461                out
3462            })
3463        })
3464        .unwrap_or_default();
3465    cm.brsl = BREND
3466        .get_or_init(|| Mutex::new(None))
3467        .lock()
3468        .ok()
3469        .and_then(|g| {
3470            g.as_ref().map(|head| {
3471                let mut out: Vec<i32> = Vec::new();
3472                let mut cur = Some(head.as_ref());
3473                while let Some(n) = cur {
3474                    out.push(n.qpos);
3475                    cur = n.next.as_deref();
3476                }
3477                out
3478            })
3479        })
3480        .unwrap_or_default();
3481
3482    cm.qipl = qipre_v.len() as i32; // c:2994
3483    cm.qisl = qisuf_v.len() as i32; // c:2995
3484                                    // c:2996 — autoq read.
3485    let autoq_v = AUTOQ
3486        .get()
3487        .and_then(|m| m.lock().ok().map(|g| g.clone()))
3488        .unwrap_or_default();
3489    cm.autoq = if !autoq_v.is_empty() {
3490        Some(autoq_v)
3491    } else if INBACKT.load(Ordering::Relaxed) != 0 {
3492        Some("`".into())
3493    } else {
3494        None
3495    };
3496
3497    cm.rems = None;
3498    cm.remf = None;
3499    cm.disp = None; // c:2997
3500
3501    // c:3003 — ai->line = join_clines(ai->line, line).
3502    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3503        if let Some(a) = g.as_mut() {
3504            let old_line = a.line.take();
3505            a.line = crate::ported::zle::compmatch::join_clines(old_line, line);
3506        }
3507    }
3508
3509    // c:3005 — mnum++.
3510    mnum.fetch_add(1, Ordering::Relaxed);
3511
3512    // c:3006 — ai->count++.
3513    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3514        if let Some(a) = g.as_mut() {
3515            a.count += 1;
3516        }
3517    }
3518
3519    // c:3008 — addlinknode((alt ? fmatches : matches), cm). Already
3520    // wired below via matches Vec push.
3521
3522    // c:3010-3011 — newmatches = 1; mgroup->new = 1.
3523    newmatches.store(1, Ordering::Relaxed);
3524
3525    // c:3012-3013 — compignored++ when alt.
3526    if alt != 0 {
3527        crate::ported::zle::complete::COMPIGNORED.fetch_add(1, Ordering::Relaxed);
3528    }
3529
3530    // c:3015-3016 — `if (!*complastprompt) dolastprompt = 0;`. Read the
3531    // `complastprompt` value (the "last_prompt" compstate, set at c:326 to
3532    // "yes"/"" from ALWAYS_LAST_PROMPT) — NOT `complastprefix`, a different
3533    // variable the previous port read by mistake. With that bug dolastprompt
3534    // was cleared on every completion, so `clearflag` stayed 0 and `trashzle`
3535    // never emitted TCCLEAREOD: an on-screen completion list was left
3536    // stranded when the command was accepted.
3537    let complastprompt_v = get_compstate_str("last_prompt").unwrap_or_default();
3538    if complastprompt_v.is_empty() {
3539        dolastprompt.store(0, Ordering::Relaxed);
3540    }
3541
3542    // c:3018-3023 — curexpl.count/fcount increment.
3543    if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
3544        if let Some(e) = g.as_mut() {
3545            if alt != 0 {
3546                e.fcount += 1;
3547            } else {
3548                e.count += 1;
3549            }
3550        }
3551    }
3552
3553    // c:3024-3025 — ai->firstm = cm.
3554    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3555        if let Some(a) = g.as_mut() {
3556            if a.firstm.is_none() {
3557                a.firstm = Some(Box::new(cm.clone()));
3558            }
3559        }
3560    }
3561
3562    // c:3027-3034 — minmlen/maxmlen tracking.
3563    let lpl = cm.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
3564    let lsl = cm.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
3565    let ml = (stl + lpl + lsl) as i32;
3566    let cur_min = minmlen.load(Ordering::Relaxed);
3567    let cur_max = maxmlen.load(Ordering::Relaxed);
3568    if ml < cur_min {
3569        minmlen.store(ml, Ordering::Relaxed);
3570    }
3571    if ml > cur_max {
3572        maxmlen.store(ml, Ordering::Relaxed);
3573    }
3574
3575    // c:3037-3064 — exact-match tracking on ai.
3576    if exact != 0 {
3577        // c:3037
3578        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3579            if let Some(a) = g.as_mut() {
3580                if a.exact == 0 {
3581                    // c:3038
3582                    a.exact = useexact.load(Ordering::Relaxed);
3583                    a.exactm = Some(Box::new(cm.clone())); // c:3058
3584                } else if useexact.load(Ordering::Relaxed) != 0 {
3585                    // c:3059
3586                    // c:3060-3061 — ambiguous exact: set to 2, clear exactm.
3587                    a.exact = 2;
3588                    a.exactm = None;
3589                }
3590            }
3591        }
3592    }
3593
3594    // c:3064 — push cm into matches/fmatches LinkList.
3595    let cell = if alt != 0 {
3596        fmatches.get_or_init(|| Mutex::new(Vec::new()))
3597    } else {
3598        matches.get_or_init(|| Mutex::new(Vec::new()))
3599    };
3600    if let Ok(mut g) = cell.lock() {
3601        g.push(cm.clone());
3602    }
3603
3604    cm // c:3067 return cm
3605}
3606
3607// `lookup_complist_flags` deleted — Rust-only 8-line helper. Inlined
3608// at the single call site in callcompfunc (c:2049-2051).
3609
3610// =====================================================================
3611// begcmgroup — `Src/Zle/compcore.c:3073`.
3612// =====================================================================
3613
3614/// Port of `mod_export void begcmgroup(char *n, int flags)` from
3615/// compcore.c:3073.
3616pub fn begcmgroup(n: Option<&str>, flags: i32) {
3617    // c:3073
3618    if let Some(name) = n {
3619        // c:3073
3620        let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON                    // c:3085
3621                 | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
3622        // c:3078-3094 — reuse an existing group with the same name+flags.
3623        let reused = {
3624            let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
3625            cell.lock().ok().and_then(|g| {
3626                g.iter()
3627                    .find(|grp| grp.name.as_deref() == Some(name) && (grp.flags & mask) == flags)
3628                    .cloned() // c:3088
3629            })
3630        };
3631        if let Some(active) = reused {
3632            // c:3090-3093 — `expls = p->lexpls; matches = p->lmatches;
3633            //   fmatches = p->lfmatches; allccs = p->lallccs;`. In C these
3634            //   are pointer aliases into the reused group so appends keep
3635            //   flowing into it. The Rust port keeps them as separate
3636            //   Mutex globals, so restore their contents from the group;
3637            //   `endcmgroup` flushes them back on close.
3638            if let Ok(mut m) = expls.get_or_init(|| Mutex::new(Vec::new())).lock() {
3639                *m = active.lexpls.clone();
3640            }
3641            if let Ok(mut m) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3642                *m = active.lmatches.clone();
3643            }
3644            if let Ok(mut m) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3645                *m = active.lfmatches.clone();
3646            }
3647            if let Ok(mut m) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock() {
3648                *m = active.lallccs.clone();
3649            }
3650            let mc = mgroup.get_or_init(|| Mutex::new(None));
3651            if let Ok(mut s) = mc.lock() {
3652                *s = Some(active);
3653            }
3654            return; // c:3095
3655        }
3656    }
3657    let mut grp = Cmgroup::default(); // c:3101
3658    grp.name = n.map(String::from); // c:3105
3659    grp.flags = flags; // c:3108
3660    let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
3661    if let Ok(mut g) = cell.lock() {
3662        g.insert(0, grp.clone()); // c:3121-3124
3663    }
3664    let mc = mgroup.get_or_init(|| Mutex::new(None));
3665    if let Ok(mut s) = mc.lock() {
3666        *s = Some(grp);
3667    }
3668    if let Ok(mut g) = expls.get_or_init(|| Mutex::new(Vec::new())).lock() {
3669        g.clear();
3670    }
3671    if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3672        g.clear();
3673    }
3674    if let Ok(mut g) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3675        g.clear();
3676    }
3677    if let Ok(mut g) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock() {
3678        g.clear();
3679    }
3680}
3681
3682// =====================================================================
3683// endcmgroup — `Src/Zle/compcore.c:3131`.
3684// =====================================================================
3685
3686/// Port of `mod_export void endcmgroup(char **ylist)` from
3687/// compcore.c:3131.
3688pub fn endcmgroup(ylist: Option<Vec<String>>) {
3689    // c:3131 — C is a one-liner (`mgroup->ylist = ylist`) because the
3690    // file-scope `matches`/`fmatches`/`expls`/`allccs` LinkLists ARE this
3691    // group's `l*` lists (aliased by begcmgroup). The Rust port keeps
3692    // those as separate Mutex globals, so on close we flush them into the
3693    // matching group inside `amatches` (identified by name+flags exactly
3694    // as begcmgroup dedups). Without this the matches added between
3695    // begcmgroup/endcmgroup never reach permmatches and `nmatches`
3696    // stays 0.
3697    let yl = ylist.unwrap_or_default();
3698
3699    // Snapshot the file-scope accumulators before touching amatches
3700    // (distinct mutexes; snapshot-first keeps the lock scopes disjoint).
3701    let m_snap = matches
3702        .get_or_init(|| Mutex::new(Vec::new()))
3703        .lock()
3704        .map(|g| g.clone())
3705        .unwrap_or_default();
3706    let fm_snap = fmatches
3707        .get_or_init(|| Mutex::new(Vec::new()))
3708        .lock()
3709        .map(|g| g.clone())
3710        .unwrap_or_default();
3711    let ex_snap = expls
3712        .get_or_init(|| Mutex::new(Vec::new()))
3713        .lock()
3714        .map(|g| g.clone())
3715        .unwrap_or_default();
3716    let ac_snap = allccs
3717        .get_or_init(|| Mutex::new(Vec::new()))
3718        .lock()
3719        .map(|g| g.clone())
3720        .unwrap_or_default();
3721
3722    // Identify the current group and record ylist on the mgroup holder.
3723    let (name, flags, new_) = {
3724        let mc = mgroup.get_or_init(|| Mutex::new(None));
3725        match mc.lock() {
3726            Ok(mut g) => match g.as_mut() {
3727                Some(grp) => {
3728                    grp.ylist = yl.clone(); // c:3140
3729                    (grp.name.clone(), grp.flags, grp.new_)
3730                }
3731                None => return,
3732            },
3733            Err(_) => return,
3734        }
3735    };
3736
3737    let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
3738    // Rust-only correctness note (no C counterpart — C aliases `matches` to
3739    // the current group's mlist so permmatches always sees live matches):
3740    // the port keeps `matches` as a separate global flushed here, so a
3741    // `permmatches` triggered mid-completion (e.g. a completer's
3742    // `$compstate[nmatches]` read) can process a still-open group BEFORE its
3743    // matches are flushed, cache nmatches with that group empty, and — since
3744    // permmatches early-returns when `newmatches==0` — never re-count it
3745    // after this flush. Every `compadd -J g2` past the first vanished. Mark
3746    // `newmatches` when a flush actually moves matches into a group so the
3747    // next permmatches recomputes instead of returning the stale cache.
3748    let flushed_any = !m_snap.is_empty() || !fm_snap.is_empty();
3749    if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3750        if let Some(grp) = g
3751            .iter_mut()
3752            .find(|grp| grp.name == name && (grp.flags & mask) == (flags & mask))
3753        {
3754            grp.lmatches = m_snap;
3755            grp.lfmatches = fm_snap;
3756            grp.lexpls = ex_snap;
3757            grp.lallccs = ac_snap;
3758            grp.ylist = yl;
3759            grp.new_ = new_;
3760        }
3761    }
3762    if flushed_any {
3763        newmatches.store(1, Ordering::Relaxed);
3764    }
3765}
3766
3767// =====================================================================
3768// addexpl — `Src/Zle/compcore.c:3140`.
3769// =====================================================================
3770
3771/// Port of `mod_export void addexpl(int always)` from compcore.c:3140.
3772pub fn addexpl(always: bool) {
3773    // c:3140
3774    let curexpl_snap = {
3775        let cell = curexpl.get_or_init(|| Mutex::new(None));
3776        cell.lock().ok().and_then(|g| g.clone())
3777    };
3778    let curexpl_str = match curexpl_snap.as_ref().and_then(|e| e.str.clone()) {
3779        Some(s) => s,
3780        None => return,
3781    };
3782    let curexpl_count = curexpl_snap.as_ref().map(|e| e.count).unwrap_or(0);
3783    let curexpl_fcount = curexpl_snap.as_ref().map(|e| e.fcount).unwrap_or(0);
3784
3785    let elist = expls.get_or_init(|| Mutex::new(Vec::new()));
3786    if let Ok(mut g) = elist.lock() {
3787        for e in g.iter_mut() {
3788            // c:3145
3789            if e.str.as_deref() == Some(curexpl_str.as_str()) {
3790                // c:3147
3791                e.count += curexpl_count; // c:3148
3792                e.fcount += curexpl_fcount; // c:3149
3793                if always {
3794                    // c:3150
3795                    e.always = 1;
3796                    nmessages.fetch_add(1, Ordering::Relaxed); // c:3152
3797                    newmatches.store(1, Ordering::Relaxed); // c:3153
3798                    let mc = mgroup.get_or_init(|| Mutex::new(None));
3799                    if let Ok(mut mg) = mc.lock() {
3800                        if let Some(grp) = mg.as_mut() {
3801                            grp.new_ = 1;
3802                        }
3803                    }
3804                }
3805                return; // c:3156
3806            }
3807        }
3808        if let Some(e) = curexpl_snap {
3809            // c:3159
3810            g.push(e);
3811        }
3812    }
3813    newmatches.store(1, Ordering::Relaxed); // c:3160
3814    if always {
3815        // c:3161
3816        let mc = mgroup.get_or_init(|| Mutex::new(None));
3817        if let Ok(mut mg) = mc.lock() {
3818            if let Some(grp) = mg.as_mut() {
3819                grp.new_ = 1;
3820            }
3821        }
3822        nmessages.fetch_add(1, Ordering::Relaxed); // c:3173
3823    }
3824}
3825
3826// =====================================================================
3827// matchcmp — `Src/Zle/compcore.c:3173`.
3828// =====================================================================
3829
3830/// Port of `static int matchcmp(Cmatch *a, Cmatch *b)` from
3831/// compcore.c:3173.
3832pub fn matchcmp(a: &Cmatch, b: &Cmatch) -> std::cmp::Ordering {
3833    // c:3173
3834    let order = MATCHORDER.load(Ordering::Relaxed);
3835    let sortdir = if (order & CGF_REVSORT) != 0 { -1 } else { 1 }; // c:3177
3836
3837    let cmp = (b.disp.is_some() as i32) - (a.disp.is_some() as i32); // c:3176
3838    let (as_, bs) = if (order & CGF_MATSORT) != 0 || (cmp == 0 && a.disp.is_none()) {
3839        (
3840            a.str.clone().unwrap_or_default(), // c:3181
3841            b.str.clone().unwrap_or_default(),
3842        ) // c:3182
3843    } else {
3844        if cmp != 0 {
3845            // c:3184
3846            let raw = (cmp as i32) * sortdir;
3847            return if raw < 0 {
3848                std::cmp::Ordering::Less
3849            }
3850            // c:3185
3851            else if raw > 0 {
3852                std::cmp::Ordering::Greater
3853            } else {
3854                std::cmp::Ordering::Equal
3855            };
3856        }
3857        let displine_cmp = (b.flags & CMF_DISPLINE) - (a.flags & CMF_DISPLINE); // c:3187
3858        if displine_cmp != 0 {
3859            // c:3188
3860            let raw = displine_cmp * sortdir;
3861            return if raw < 0 {
3862                std::cmp::Ordering::Less
3863            } else if raw > 0 {
3864                std::cmp::Ordering::Greater
3865            } else {
3866                std::cmp::Ordering::Equal
3867            };
3868        }
3869        (
3870            a.disp.clone().unwrap_or_default(), // c:3191
3871            b.disp.clone().unwrap_or_default(),
3872        ) // c:3192
3873    };
3874    // c:3195-3197 — `sortdir * zstrcmp(as, bs, SORTIT_IGNORING_BACKSLASHES |
3875    // ((isset(NUMERICGLOBSORT) || matchorder & CGF_NUMSORT) ?
3876    //   SORTIT_NUMERICALLY : 0))`. zstrcmp routes through strcoll, so the
3877    // ordering is the locale's collation (case-insensitive on the usual
3878    // macOS/Linux locales) — matching zsh. The previous byte comparison
3879    // sorted ASCII, putting every uppercase name (`README.md`) ahead of
3880    // lowercase ones (`alpha.txt`), which diverged from zsh's completion
3881    // order.
3882    let numeric = crate::ported::zsh_h::isset(crate::ported::zsh_h::NUMERICGLOBSORT)
3883        || (order & CGF_NUMSORT) != 0;
3884    let flags = crate::ported::zsh_h::SORTIT_IGNORING_BACKSLASHES as u32
3885        | if numeric {
3886            crate::ported::zsh_h::SORTIT_NUMERICALLY as u32
3887        } else {
3888            0
3889        };
3890    let base = crate::ported::sort::zstrcmp(&as_, &bs, flags);
3891    if sortdir < 0 {
3892        base.reverse()
3893    } else {
3894        base
3895    }
3896}
3897
3898/// Port of `static int matcheq(Cmatch a, Cmatch b)` from
3899/// compcore.c:3206.
3900pub fn matcheq(a: &Cmatch, b: &Cmatch) -> bool {
3901    // c:3207
3902    matchstreq(a.ipre.as_ref(),  b.ipre.as_ref())  &&                        // c:3207
3903    matchstreq(a.pre.as_ref(),   b.pre.as_ref())   &&                        // c:3210
3904    matchstreq(a.ppre.as_ref(),  b.ppre.as_ref())  &&                        // c:3211
3905    matchstreq(a.psuf.as_ref(),  b.psuf.as_ref())  &&                        // c:3212
3906    matchstreq(a.suf.as_ref(),   b.suf.as_ref())   &&                        // c:3213
3907    matchstreq(a.str.as_ref(),  b.str.as_ref()) // c:3214
3908}
3909
3910// =====================================================================
3911// makearray — `Src/Zle/compcore.c:3224`.
3912// =====================================================================
3913
3914/// Port of `static Cmatch *makearray(LinkList l, int type, int flags,
3915///                                    int *np, int *nlp, int *llp)`
3916/// from compcore.c:3223. Returns `(arr, n, nl, ll)`.
3917///
3918/// `type` is fixed to `1` (match-sort path) for the in-file call sites
3919/// from `permmatches`. The `type=0` string-sort path on `lexpls` is
3920/// inlined at the `permmatches` call site (C uses a `(char **)` cast
3921/// trick that has no safe Rust equivalent).
3922pub fn makearray(mut rp: Vec<Cmatch>, flags: i32) -> (Vec<Cmatch>, i32, i32, i32) {
3923    // c:3224
3924    let mut n: i32 = rp.len() as i32; // c:3224
3925    let mut nl: i32 = 0; // c:3231
3926    let mut ll: i32 = 0; // c:3231
3927
3928    if n > 0 {
3929        // c:3258 (type==1 branch)
3930        if (flags & CGF_NOSORT) == 0 {
3931            // c:3259
3932            // Now sort the array (it contains matches).                     // c:3260
3933            MATCHORDER.store(flags, Ordering::Relaxed); // c:3261
3934            // c:3262 — C `qsort(rp, n, sizeof(Cmatch), matchcmp)`. Must use the
3935            // qsort-tolerant sort: matchcmp→zstrcmp is not a strict weak order
3936            // (numeric/natural sort), which makes Rust's sort_by PANIC.
3937            crate::tolerant_sort::qsort_tolerant(&mut rp, matchcmp);
3938
3939            if (flags & CGF_UNIQCON) == 0 {
3940                // c:3269 not -2
3941                // remove dupes
3942                let mut cp = 0usize; // c:3272
3943                let mut ap = 0usize;
3944                while ap < rp.len() {
3945                    // c:3274 for ap;*ap;ap++
3946                    // Scan the run of duplicates FIRST, using the element at
3947                    // `ap` (C keeps `*ap` stable — `*cp++ = *ap` copies, it
3948                    // does not move). Doing `rp.swap(ap, cp)` before this scan
3949                    // (the old code) overwrote `rp[ap]` with a stale slot once
3950                    // any earlier removal made `cp < ap`, so `rp[ap].str ==
3951                    // rp[bp+1].str` compared the wrong element and same-string
3952                    // duplicates (e.g. `libpng-config` from several $path dirs)
3953                    // were never collapsed.
3954                    let mut bp = ap;
3955                    while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
3956                        bp += 1;
3957                        n -= 1; // c:3277 bp[1] && matcheq
3958                    }
3959                    let mut dup = 0i32; // c:3281
3960                    while bp + 1 < rp.len()
3961                        && rp[ap].disp.is_none()
3962                        && rp[bp + 1].disp.is_none()                         // c:3282 !disp
3963                        && rp[ap].str == rp[bp + 1].str
3964                    {
3965                        rp[bp + 1].flags |= CMF_MULT; // c:3284
3966                        dup = 1; // c:3285
3967                        bp += 1;
3968                        n -= 1; // same-string duplicate is dropped too
3969                    }
3970                    if dup != 0 {
3971                        // c:3287
3972                        rp[ap].flags |= CMF_FMULT; // c:3288
3973                    }
3974                    // c:3275 `*cp++ = *ap` — keep the first of the run at `cp`.
3975                    if ap != cp {
3976                        rp.swap(ap, cp);
3977                    }
3978                    cp += 1;
3979                    ap = bp + 1; // c:3279 ap = bp; ap++
3980                }
3981                rp.truncate(cp); // c:3291 *cp = NULL
3982            }
3983            for m in rp.iter() {
3984                // c:3293
3985                if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
3986                    // c:3294
3987                    ll += 1;
3988                }
3989                if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
3990                    // c:3296
3991                    nl += 1;
3992                }
3993            }
3994        } else {
3995            // c:3300 used -O nosort or -V
3996            if (flags & CGF_UNIQALL) == 0 && (flags & CGF_UNIQCON) == 0 {
3997                // c:3302 didn't use -1 or -2
3998                MATCHORDER.store(flags, Ordering::Relaxed); // c:3306
3999                let mut sp: Vec<Cmatch> = rp.clone(); // c:3309-3312 zhalloc + memcpy
4000                // c:3313 — qsort matchcmp; tolerant sort (non-total-order cmp).
4001                crate::tolerant_sort::qsort_tolerant(&mut sp, matchcmp);
4002
4003                let mut del = false; // c:3303
4004                                     // Sweep sorted dup-detection back onto rp via flag marks.
4005                for w in sp.windows(2) {
4006                    // c:3315-3329
4007                    if matcheq(&w[0], &w[1]) {
4008                        // Mark in original rp by str+disp equality.
4009                        for m in rp.iter_mut() {
4010                            if matcheq(m, &w[1]) {
4011                                m.flags = CMF_DELETE; // c:3318
4012                                del = true; // c:3319
4013                                break;
4014                            }
4015                        }
4016                    } else if w[0].disp.is_none() {
4017                        if w[1].disp.is_none() && w[0].str == w[1].str {
4018                            // c:3322
4019                            for m in rp.iter_mut() {
4020                                if matcheq(m, &w[1]) {
4021                                    m.flags |= CMF_MULT; // c:3324
4022                                    break;
4023                                }
4024                            }
4025                            for m in rp.iter_mut() {
4026                                if matcheq(m, &w[0]) {
4027                                    m.flags |= CMF_FMULT; // c:3328
4028                                    break;
4029                                }
4030                            }
4031                        }
4032                    }
4033                }
4034                if del {
4035                    // c:3332
4036                    rp.retain(|m| (m.flags & CMF_DELETE) == 0); // c:3334-3340
4037                    n = rp.len() as i32;
4038                }
4039            } else if (flags & CGF_UNIQCON) == 0 {
4040                // c:3344 -1 not -2
4041                let mut cp = 0usize;
4042                let mut ap = 0usize;
4043                while ap < rp.len() {
4044                    // c:3346
4045                    if ap != cp {
4046                        rp.swap(ap, cp);
4047                    }
4048                    cp += 1;
4049                    let mut bp = ap;
4050                    while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
4051                        bp += 1;
4052                        n -= 1; // c:3348
4053                    }
4054                    let mut dup = 0i32;
4055                    while bp + 1 < rp.len()
4056                        && rp[ap].disp.is_none()
4057                        && rp[bp + 1].disp.is_none()
4058                        && rp[ap].str == rp[bp + 1].str
4059                    {
4060                        rp[bp + 1].flags |= CMF_MULT; // c:3352
4061                        dup = 1; // c:3353
4062                        bp += 1;
4063                    }
4064                    if dup != 0 {
4065                        rp[ap].flags |= CMF_FMULT; // c:3356
4066                    }
4067                    ap = bp + 1;
4068                }
4069                rp.truncate(cp); // c:3359
4070            }
4071            for m in rp.iter() {
4072                // c:3361
4073                if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
4074                    // c:3362
4075                    ll += 1;
4076                }
4077                if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
4078                    // c:3364
4079                    nl += 1;
4080                }
4081            }
4082        }
4083    }
4084    (rp, n, nl, ll) // c:3366-3373
4085}
4086
4087// =====================================================================
4088// dupmatch — `Src/Zle/compcore.c:3370`.
4089// =====================================================================
4090
4091/// Port of `static Cmatch dupmatch(Cmatch m, int nbeg, int nend)` from
4092/// compcore.c:3370. Deep-copies one match; brpl/brsl are truncated to
4093/// nbeg/nend per the C body's nbeg/nend-sized `zalloc` + element copy.
4094pub fn dupmatch(m: &Cmatch, nbeg: i32, nend: i32) -> Cmatch {
4095    // c:3370
4096    let mut r = Cmatch::default(); // c:3370-3374
4097    r.str = m.str.clone(); // c:3376 ztrdup
4098    r.orig = m.orig.clone(); // c:3377
4099    r.ipre = m.ipre.clone(); // c:3378
4100    r.ripre = m.ripre.clone(); // c:3379
4101    r.isuf = m.isuf.clone(); // c:3380
4102    r.ppre = m.ppre.clone(); // c:3381
4103    r.psuf = m.psuf.clone(); // c:3382
4104    r.prpre = m.prpre.clone(); // c:3383
4105    r.pre = m.pre.clone(); // c:3384
4106    r.suf = m.suf.clone(); // c:3385
4107    r.flags = m.flags; // c:3386
4108    if !m.brpl.is_empty() {
4109        // c:3387
4110        let take = (nbeg as usize).min(m.brpl.len()); // c:3390 zalloc(nbeg)
4111        r.brpl = m.brpl[..take].to_vec(); // c:3392 element-wise copy
4112    } else {
4113        r.brpl = Vec::new(); // c:3395 NULL
4114    }
4115    if !m.brsl.is_empty() {
4116        // c:3396
4117        let take = (nend as usize).min(m.brsl.len()); // c:3399
4118        r.brsl = m.brsl[..take].to_vec(); // c:3401
4119    } else {
4120        r.brsl = Vec::new(); // c:3404
4121    }
4122    r.rems = m.rems.clone(); // c:3405
4123    r.remf = m.remf.clone(); // c:3406
4124    r.autoq = m.autoq.clone(); // c:3407
4125    r.qipl = m.qipl; // c:3408
4126    r.qisl = m.qisl; // c:3409
4127    r.disp = m.disp.clone(); // c:3410
4128    r.mode = m.mode; // c:3411
4129    r.modec = m.modec; // c:3412
4130    r.fmode = m.fmode; // c:3413
4131    r.fmodec = m.fmodec; // c:3414
4132    r // c:3416
4133}
4134
4135/// Port of `mod_export int permmatches(int last)` from compcore.c:3422.
4136/// Promotes the per-round `amatches` accumulator into the permanent
4137/// `pmatches` snapshot via deep-copy through `dupmatch`/`makearray`.
4138pub fn permmatches(last: i32) -> i32 {
4139    // c:3423
4140    let ofi = PERMMATCHES_FI.load(Ordering::Relaxed); // c:3423 ofi = fi
4141
4142    // c:3433 — `if (pmatches && !newmatches)`
4143    let pmatches_set = pmatches
4144        .get_or_init(|| Mutex::new(Vec::new()))
4145        .lock()
4146        .map(|g| !g.is_empty())
4147        .unwrap_or(false);
4148    if pmatches_set && newmatches.load(Ordering::Relaxed) == 0 {
4149        // c:3433
4150        if last != 0 && PERMMATCHES_FI.load(Ordering::Relaxed) != 0 {
4151            // c:3434
4152            // ainfo = fainfo                                                // c:3435
4153            let famref = fainfo
4154                .get_or_init(|| Mutex::new(None))
4155                .lock()
4156                .ok()
4157                .and_then(|g| g.clone());
4158            if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4159                *a = famref;
4160            }
4161        }
4162        return PERMMATCHES_FI.load(Ordering::Relaxed); // c:3437
4163    }
4164    newmatches.store(0, Ordering::Relaxed); // c:3439
4165    PERMMATCHES_FI.store(0, Ordering::Relaxed); // c:3439 fi = 0
4166
4167    {
4168        // pmatches = lmatches = NULL                                        // c:3441
4169        if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
4170            g.clear();
4171        }
4172        if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
4173            *g = None;
4174        }
4175    }
4176    nmatches.store(0, Ordering::Relaxed); // c:3442
4177    smatches.store(0, Ordering::Relaxed); // c:3442
4178    diffmatches.store(0, Ordering::Relaxed); // c:3442
4179
4180    // c:3444 — `if (!ainfo->count)`.
4181    let ainfo_count = ainfo
4182        .get_or_init(|| Mutex::new(None))
4183        .lock()
4184        .ok()
4185        .and_then(|g| g.as_ref().map(|a| a.count))
4186        .unwrap_or(0);
4187    if ainfo_count == 0 {
4188        // c:3444
4189        if last != 0 {
4190            // c:3445
4191            let famref = fainfo
4192                .get_or_init(|| Mutex::new(None))
4193                .lock()
4194                .ok()
4195                .and_then(|g| g.clone());
4196            if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4197                *a = famref;
4198            }
4199        }
4200        PERMMATCHES_FI.store(1, Ordering::Relaxed); // c:3447
4201    }
4202
4203    let nbeg = NBRBEG.load(Ordering::Relaxed);
4204    let nend = NBREND.load(Ordering::Relaxed);
4205
4206    let mut gn: i32 = 1; // c:3429 gn = 1
4207    let mut mn: i32 = 1; // c:3429 mn = 1
4208    let fi = PERMMATCHES_FI.load(Ordering::Relaxed);
4209
4210    let groups_snapshot: Vec<Cmgroup> = {
4211        amatches
4212            .get_or_init(|| Mutex::new(Vec::new()))
4213            .lock()
4214            .ok()
4215            .map(|g| g.clone())
4216            .unwrap_or_default()
4217    };
4218    let mut new_pmatches: Vec<Cmgroup> = Vec::with_capacity(groups_snapshot.len());
4219
4220    for g_orig in groups_snapshot.into_iter() {
4221        // c:3449 while (g)
4222        let mut g = g_orig; // borrow-mut snapshot
4223        let must_rebuild = fi != ofi || g.perm.is_none() || g.new_ != 0; // c:3456
4224        if must_rebuild {
4225            // c:3456
4226            let src_list = if fi != 0 {
4227                g.lfmatches.clone()
4228            }
4229            // c:3457
4230            else {
4231                g.lmatches.clone()
4232            }; // c:3461
4233
4234            let (arr, nn, nl, ll) = makearray(src_list, g.flags); // c:3463
4235            g.mcount = nn; // c:3464
4236            g.lcount = nn - nl; // c:3465
4237            if g.lcount < 0 {
4238                g.lcount = 0;
4239            } // c:3466
4240            g.llcount = ll; // c:3467
4241            if !g.ylist.is_empty() {
4242                // c:3468
4243                g.lcount = g.ylist.len() as i32; // c:3469
4244                smatches.store(2, Ordering::Relaxed); // c:3470
4245            }
4246            // c:3472 — makearray(lexpls, 0, 0, &ecount, NULL, NULL).
4247            let mut exps = g.lexpls.clone(); // type=0 path
4248            g.ecount = exps.len() as i32;
4249            // c:3475 ccount = 0
4250            g.ccount = 0; // c:3475
4251            tracing::debug!(
4252                target: "compsys_args",
4253                mcount = g.mcount,
4254                lcount = g.lcount,
4255                nn,
4256                nl,
4257                ll,
4258                name = ?g.name,
4259                "permmatches group"
4260            );
4261            nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3477
4262            smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3478
4263            if g.mcount > 1 {
4264                // c:3480
4265                diffmatches.store(1, Ordering::Relaxed); // c:3481
4266            }
4267
4268            // n = (Cmgroup) zshcalloc(...)                                  // c:3483
4269            let mut n_grp = Cmgroup::default();
4270            // c:3487 — `if (g->perm) freematches(g->perm, 0)`. Drop on
4271            // perm Box<Cmgroup> reclaims the C `free` path.
4272            g.perm = None; // c:3490 g->perm = n
4273                           // Then below we set g.perm = Some(Box::new(n_grp.clone())).
4274
4275            n_grp.num = gn;
4276            gn += 1; // c:3499
4277            n_grp.flags = g.flags; // c:3500
4278            n_grp.mcount = g.mcount; // c:3501
4279            n_grp.matches = arr
4280                .iter() // c:3502-3505 dupmatch loop
4281                .map(|m| dupmatch(m, nbeg, nend))
4282                .collect();
4283            n_grp.name = g.name.clone(); // c:3504
4284            n_grp.lcount = g.lcount; // c:3508
4285            n_grp.llcount = g.llcount; // c:3509
4286            if !g.ylist.is_empty() {
4287                // c:3510
4288                n_grp.ylist = g.ylist.clone(); // c:3511 zarrdup
4289            } else {
4290                n_grp.ylist = Vec::new(); // c:3513
4291            }
4292            if g.ecount != 0 {
4293                // c:3515
4294                // Build n->expls from g->expls deep-copying str + (fi
4295                // ? fcount : count); always carries over; fcount = 0.
4296                n_grp.expls = exps
4297                    .drain(..)
4298                    .map(|o| Cexpl {
4299                        // c:3517-3525
4300                        count: if fi != 0 { o.fcount } else { o.count }, // c:3520
4301                        always: o.always,                                // c:3521
4302                        fcount: 0,                                       // c:3522
4303                        str: o.str.clone(),                              // c:3523 ztrdup
4304                    })
4305                    .collect();
4306                n_grp.ecount = g.ecount;
4307            } else {
4308                n_grp.expls = Vec::new(); // c:3528
4309            }
4310            n_grp.widths = Vec::new(); // c:3531
4311                                       // Stitch perm chain (prev/next handled implicitly by Vec).
4312            g.matches = arr; // mirror C: g->matches = makearray result
4313            g.perm = Some(Box::new(n_grp.clone())); // c:3490 g->perm = n
4314            new_pmatches.push(n_grp); // c:3492-3496
4315        } else {
4316            // reuse existing g->perm                                        // c:3534
4317            nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3540
4318            smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3541
4319            if g.mcount > 1 {
4320                diffmatches.store(1, Ordering::Relaxed); // c:3543
4321            }
4322            g.num = gn;
4323            gn += 1; // c:3546
4324            if let Some(p) = g.perm.as_deref() {
4325                new_pmatches.push(p.clone()); // c:3537 pmatches = g->perm
4326            }
4327        }
4328        g.new_ = 0; // c:3548
4329    }
4330
4331    // c:3490-3538 — C threads each group onto `pmatches` via
4332    // `n->next = pmatches; pmatches = n` (a PREPEND), so pmatches ends up
4333    // in the REVERSE of amatches iteration order. The port accumulates into
4334    // a Vec with push() (append), so reverse once here to recover C's order.
4335    // Without this, `group-order`/`group-name` groups list in creation order
4336    // reversed (e.g. `group-order veggies fruits` showed fruits first), and
4337    // the gnum/rnum numbering below — which C runs over the reversed
4338    // pmatches — was assigned against the wrong sequence.
4339    new_pmatches.reverse();
4340
4341    // c:3551-3563 — assign rnum/gnum, recompute diffmatches/nbrbeg.
4342    let mut first_first: Option<Cmatch> = None;
4343    for g_pm in new_pmatches.iter_mut() {
4344        g_pm.nbrbeg = nbeg; // c:3552
4345        g_pm.nbrend = nend; // c:3553
4346        let mut rn = 1i32; // c:3554
4347        for m in g_pm.matches.iter_mut() {
4348            m.rnum = rn;
4349            rn += 1; // c:3555
4350            m.gnum = mn;
4351            mn += 1; // c:3556
4352        }
4353        if diffmatches.load(Ordering::Relaxed) == 0 && !g_pm.matches.is_empty() {
4354            match first_first.as_ref() {
4355                // c:3558
4356                Some(p0) => {
4357                    if !matcheq(&g_pm.matches[0], p0) {
4358                        diffmatches.store(1, Ordering::Relaxed); // c:3560
4359                    }
4360                }
4361                None => first_first = Some(g_pm.matches[0].clone()), // c:3562
4362            }
4363        }
4364    }
4365
4366    if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
4367        *g = new_pmatches;
4368    }
4369
4370    hasperm.store(1, Ordering::Relaxed); // c:3565
4371    permmnum.store(mn - 1, Ordering::Relaxed); // c:3566
4372    permgnum.store(gn - 1, Ordering::Relaxed); // c:3567
4373    if let Ok(mut ld) = listdat
4374        .get_or_init(|| Mutex::new(Default::default()))
4375        .lock()
4376    {
4377        ld.valid = 0; // c:3568
4378    }
4379
4380    fi // c:3570
4381}
4382
4383// =====================================================================
4384// freematch / freematches — `Src/Zle/compcore.c:3575 / 3605`.
4385// =====================================================================
4386
4387/// Port of `static void freematch(Cmatch m, int nbeg, int nend)` from
4388/// `Src/Zle/compcore.c:3575`. C frees each Cmatch field via `zsfree`
4389/// (str/orig/ipre/ripre/isuf/ppre/psuf/pre/suf/prpre/rems/remf/disp/
4390/// autoq) and `zfree(m->brpl, nbeg * sizeof(int))` /
4391/// `zfree(m->brsl, nend * sizeof(int))` — all collapse to Rust's
4392/// automatic Drop on the owned String / `Vec<i32>` fields. nbeg/nend
4393/// kept on the signature for C parity (consumed by C as `zfree` size
4394/// args; Rust Vec carries its own length).
4395pub fn freematch(m: Cmatch, _nbeg: i32, _nend: i32) {
4396    // c:3577 — `if (!m) return;` — Rust's owned `m` is always valid;
4397    // dropping it on return runs every field's destructor (c:3579-3596
4398    // zsfree / zfree calls collapsed).
4399    drop(m); // c:3598 zfree(m)
4400}
4401
4402/// Direct port of `mod_export void freematches(Cmgroup g, int cm)` from
4403/// `Src/Zle/compcore.c:3605`. The C path walks the cmgroup chain freeing
4404/// each Cmatch + ylist + expls + widths + name; in Rust those are
4405/// owned by `Vec`/`Box`/`String` so Drop covers the per-node free.
4406/// The `cm` arm at c:3636-3637 (`minfo.cur = NULL`) is the only
4407/// side-effect that doesn't fall out of Rust's ownership model — wire
4408/// it explicitly.
4409pub fn freematches(g: Vec<Cmgroup>, cm: i32) {
4410    // c:3605
4411    drop(g);
4412    if cm != 0 {
4413        // c:3636
4414        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4415            g.cur = None; // c:3637
4416        }
4417    }
4418}
4419
4420// =====================================================================
4421// Extern globals — declared in other C files, mirrored here per
4422// PORT.md Rule 9 ("stub the EXTERN dependencies ... locally with
4423// file:line citations to their home file") so the local body ports
4424// below have a value source. When the canonical Rust homes land,
4425// these become `pub use crate::ported::<canonical>::*` re-exports.
4426// =====================================================================
4427
4428/// Port of `mod_export int lastend` from `Src/Zle/compcore.c:276`.
4429/// Byte position in the metafied line where the most-recent
4430/// completion insertion ended.
4431///
4432/// Re-export alias of [`lastend`] — C has ONE `lastend` (compcore.c:276).
4433/// Two atomics existed: `compresult` wrote the lowercase `lastend`, while
4434/// `complist`'s menu-list positioning read this uppercase `LASTEND`, which was
4435/// never stored (always 0) — so menu-selection column math used a stale zero.
4436pub use self::lastend as LASTEND;
4437
4438/// Port of `mod_export int wb` from `Src/lex.c:120`. Word-begin
4439/// position in the metafied line for the currently-completing word.
4440pub static WB: AtomicI32 = AtomicI32::new(0); // lex.c:120
4441/// Port of `mod_export int we` from `Src/lex.c:120`. Word-end position.
4442pub static WE: AtomicI32 = AtomicI32::new(0); // lex.c:120
4443/// Port of `mod_export int zlemetacs` from `Src/lex.c:104`. Cursor
4444/// position in the metafied line.
4445pub static ZLEMETACS: AtomicI32 = AtomicI32::new(0); // lex.c:104
4446/// Port of `mod_export int zlemetall` from `Src/lex.c:104`. Length
4447/// of the metafied line.
4448pub static ZLEMETALL: AtomicI32 = AtomicI32::new(0); // lex.c:104
4449/// Port of `mod_export int addedx` from `Src/lex.c:115`. Non-zero
4450/// while a dummy `x` cursor marker is in the line being lexed
4451/// (so completion can capture the partial word at the cursor).
4452pub static ADDEDX: AtomicI32 = AtomicI32::new(0); // lex.c:115
4453
4454/// Port of `mod_export char *zlemetaline` from `Src/lex.c:103`. The
4455/// metafied edit buffer for the current ZLE session — `foredel`,
4456/// `inststr`, `selfinsert` operate on this directly when compcore's
4457/// error-recovery path fires (compcore.c:344-355).
4458pub static ZLEMETALINE: OnceLock<Mutex<String>> = OnceLock::new(); // lex.c:103
4459/// Port of `mod_export ZLE_STRING_T zleline` from `Src/zle_main.c`.
4460pub static ZLELINE: OnceLock<Mutex<String>> = OnceLock::new(); // zle_main.c
4461/// Port of `mod_export int zlecs` from `Src/zle_main.c`.
4462pub static ZLECS: AtomicI32 = AtomicI32::new(0); // zle_main.c
4463/// Port of `mod_export int zlell` from `Src/zle_main.c`.
4464pub static ZLELL: AtomicI32 = AtomicI32::new(0); // zle_main.c
4465/// Port of `mod_export int inwhat` from `Src/lex.c:110`. Lex context
4466/// classification — IN_NOTHING / IN_CMD / IN_COND / IN_MATH / IN_PAR /
4467/// IN_ENV.
4468pub static INWHAT: AtomicI32 = AtomicI32::new(0); // lex.c:110
4469/// Port of `mod_export int zmult` from `Src/zle_main.c`. Numeric
4470/// prefix multiplier for the current ZLE command.
4471pub static ZMULT: AtomicI32 = AtomicI32::new(1); // zle_main.c
4472/// Port of `mod_export char *compfunc` from `Src/Zle/zle_tricky.c:143`.
4473/// Name of the user completion shell function — non-empty when the
4474/// new completion system (`compsys`) is active; empty for compctl.
4475pub static compfunc: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // zle_tricky.c:143
4476/// Port of `mod_export char *comppatmatch` from `Src/Zle/zle_tricky.c`.
4477/// `$compstate[pattern_match]` — when non-empty + non-"\0" enables
4478/// pattern-aware matching for parameter-name completion.
4479pub static comppatmatch: OnceLock<Mutex<Option<String>>> = OnceLock::new();
4480// `compqstack` (C: complete.c `mod_export char *compqstack`) is deduped
4481// to the single canonical `complete::COMPQSTACK`, imported at the top of
4482// this module. The former compcore-local `compqstack` static was an
4483// orphan: the c:305 reset wrote it while `multiquote` (c:1065) read the
4484// imported COMPQSTACK, so the reset never reached its reader — a real
4485// bug now fixed by pointing both at COMPQSTACK.
4486// =====================================================================
4487// File-scope globals — `Src/Zle/compcore.c:36-279`.
4488// =====================================================================
4489
4490/// Port of `int useexact` from compcore.c:36.
4491pub static useexact: AtomicI32 = AtomicI32::new(0); // c:36
4492/// Port of `int useline` from compcore.c:36.
4493pub static useline: AtomicI32 = AtomicI32::new(0); // c:36
4494/// Port of `int uselist` from compcore.c:36.
4495pub static uselist: AtomicI32 = AtomicI32::new(0); // c:36
4496/// Port of `int forcelist` from compcore.c:36.
4497pub static forcelist: AtomicI32 = AtomicI32::new(0); // c:36
4498/// Port of `int startauto` from compcore.c:36.
4499pub static startauto: AtomicI32 = AtomicI32::new(0); // c:36
4500
4501/// Port of `mod_export int iforcemenu` from compcore.c:39.
4502pub static iforcemenu: AtomicI32 = AtomicI32::new(0); // c:39
4503
4504/// Port of `mod_export int dolastprompt` from compcore.c:44.
4505pub static dolastprompt: AtomicI32 = AtomicI32::new(0); // c:44
4506
4507/// Port of `mod_export int oldlist` from compcore.c:49.
4508pub static oldlist: AtomicI32 = AtomicI32::new(0); // c:49
4509/// Port of `mod_export int oldins` from compcore.c:49.
4510pub static oldins: AtomicI32 = AtomicI32::new(0); // c:49
4511
4512/// Port of `int origlpre` from compcore.c:54.
4513pub static origlpre: AtomicI32 = AtomicI32::new(0); // c:54
4514/// Port of `int origlsuf` from compcore.c:54.
4515pub static origlsuf: AtomicI32 = AtomicI32::new(0); // c:54
4516/// Port of `int lenchanged` from compcore.c:54.
4517pub static lenchanged: AtomicI32 = AtomicI32::new(0); // c:54
4518
4519/// Port of `int movetoend` from compcore.c:61.
4520pub static movetoend: AtomicI32 = AtomicI32::new(0); // c:61
4521
4522/// Port of `mod_export int insmnum` from compcore.c:66.
4523pub static insmnum: AtomicI32 = AtomicI32::new(0); // c:66
4524/// Port of `mod_export int insspace` from compcore.c:66.
4525pub static insspace: AtomicI32 = AtomicI32::new(0); // c:66
4526
4527/// Port of `mod_export int menuacc` from compcore.c:81.
4528pub static menuacc: AtomicI32 = AtomicI32::new(0); // c:81
4529
4530/// Port of `int hasunqu` from compcore.c:86.
4531pub static hasunqu: AtomicI32 = AtomicI32::new(0); // c:86
4532/// Port of `int useqbr` from compcore.c:86.
4533pub static useqbr: AtomicI32 = AtomicI32::new(0); // c:86
4534/// Port of `int brpcs` from compcore.c:86.
4535pub static brpcs: AtomicI32 = AtomicI32::new(0); // c:86
4536/// Port of `int brscs` from compcore.c:86.
4537pub static brscs: AtomicI32 = AtomicI32::new(0); // c:86
4538
4539/// Port of `mod_export int ispar` from compcore.c:91.
4540pub static ispar: AtomicI32 = AtomicI32::new(0); // c:91
4541/// Port of `mod_export int linwhat` from compcore.c:91.
4542pub static linwhat: AtomicI32 = AtomicI32::new(0); // c:91
4543
4544/// Port of `char *parpre` from compcore.c:96.
4545pub static parpre: OnceLock<Mutex<String>> = OnceLock::new(); // c:96
4546
4547/// Port of `int parflags` from compcore.c:101.
4548pub static parflags: AtomicI32 = AtomicI32::new(0); // c:101
4549
4550/// Port of `mod_export int mflags` from compcore.c:106.
4551pub static mflags: AtomicI32 = AtomicI32::new(0); // c:106
4552
4553/// Port of `int parq` from compcore.c:111.
4554pub static parq: AtomicI32 = AtomicI32::new(0); // c:111
4555/// Port of `int eparq` from compcore.c:111.
4556pub static eparq: AtomicI32 = AtomicI32::new(0); // c:111
4557
4558/// Port of `mod_export char *ipre` from compcore.c:118.
4559pub static ipre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
4560/// Port of `mod_export char *ripre` from compcore.c:118.
4561pub static ripre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
4562/// Port of `mod_export char *isuf` from compcore.c:118.
4563pub static isuf: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
4564
4565/// Port of `mod_export LinkList matches` from compcore.c:124.
4566pub static matches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:124
4567/// Port of `LinkList fmatches` from compcore.c:126.
4568pub static fmatches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:126
4569
4570/// Port of `mod_export Cmgroup amatches` from compcore.c:135.
4571pub static amatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
4572/// Port of `mod_export Cmgroup pmatches` from compcore.c:135.
4573pub static pmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
4574/// Port of `mod_export Cmgroup lastmatches` from compcore.c:135.
4575pub static lastmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
4576/// Port of `mod_export Cmgroup lmatches` from compcore.c:135. Last
4577/// element pointer in the perm list; here a single-slot holder.
4578pub static lmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
4579/// Port of `mod_export Cmgroup lastlmatches` from compcore.c:135.
4580pub static lastlmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
4581
4582/// Port of `mod_export int hasoldlist` from compcore.c:140.
4583pub static hasoldlist: AtomicI32 = AtomicI32::new(0); // c:140
4584/// Port of `mod_export int hasperm` from compcore.c:140.
4585pub static hasperm: AtomicI32 = AtomicI32::new(0); // c:140
4586/// Port of `int hasallmatch` from compcore.c:145.
4587pub static hasallmatch: AtomicI32 = AtomicI32::new(0); // c:145
4588
4589/// Port of `mod_export int newmatches` from compcore.c:150.
4590pub static newmatches: AtomicI32 = AtomicI32::new(0); // c:150
4591
4592/// Port of `mod_export int permmnum` from compcore.c:155.
4593pub static permmnum: AtomicI32 = AtomicI32::new(0); // c:155
4594/// Port of `mod_export int permgnum` from compcore.c:155.
4595pub static permgnum: AtomicI32 = AtomicI32::new(0); // c:155
4596/// Port of `mod_export int lastpermmnum` from compcore.c:155.
4597pub static lastpermmnum: AtomicI32 = AtomicI32::new(0); // c:155
4598/// Port of `mod_export int lastpermgnum` from compcore.c:155.
4599pub static lastpermgnum: AtomicI32 = AtomicI32::new(0); // c:155
4600
4601/// Port of `mod_export int nmatches` from compcore.c:160.
4602pub static nmatches: AtomicI32 = AtomicI32::new(0); // c:160
4603/// Port of `mod_export int smatches` from compcore.c:162.
4604pub static smatches: AtomicI32 = AtomicI32::new(0); // c:162
4605
4606/// Port of `mod_export int diffmatches` from compcore.c:167.
4607pub static diffmatches: AtomicI32 = AtomicI32::new(0); // c:167
4608
4609/// Port of `mod_export int nmessages` from compcore.c:172.
4610pub static nmessages: AtomicI32 = AtomicI32::new(0); // c:172
4611
4612/// Port of `mod_export int onlyexpl` from compcore.c:177.
4613pub static onlyexpl: AtomicI32 = AtomicI32::new(0); // c:177
4614
4615/// Port of `mod_export struct cldata listdat` from compcore.c:182.
4616pub static listdat: OnceLock<Mutex<crate::ported::zle::comp_h::Cldata>> = OnceLock::new(); // c:182
4617
4618/// Port of `mod_export int ispattern` from compcore.c:187.
4619pub static ispattern: AtomicI32 = AtomicI32::new(0); // c:187
4620/// Port of `mod_export int haspattern` from compcore.c:187.
4621pub static haspattern: AtomicI32 = AtomicI32::new(0); // c:187
4622
4623/// Port of `mod_export int hasmatched` from compcore.c:192.
4624pub static hasmatched: AtomicI32 = AtomicI32::new(0); // c:192
4625/// Port of `mod_export int hasunmatched` from compcore.c:192.
4626pub static hasunmatched: AtomicI32 = AtomicI32::new(0); // c:192
4627
4628/// Port of `Cmgroup mgroup` from compcore.c:197.
4629pub static mgroup: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:197
4630
4631/// Port of `mod_export int mnum` from compcore.c:202.
4632pub static mnum: AtomicI32 = AtomicI32::new(0); // c:202
4633
4634/// Port of `mod_export int unambig_mnum` from compcore.c:207.
4635pub static unambig_mnum: AtomicI32 = AtomicI32::new(0); // c:207
4636
4637/// Port of `int maxmlen` from compcore.c:212.
4638pub static maxmlen: AtomicI32 = AtomicI32::new(0); // c:212
4639/// Port of `int minmlen` from compcore.c:212.
4640pub static minmlen: AtomicI32 = AtomicI32::new(0); // c:212
4641
4642/// Port of `LinkList expls` from compcore.c:218.
4643pub static expls: OnceLock<Mutex<Vec<Cexpl>>> = OnceLock::new(); // c:218
4644
4645/// Port of `mod_export Cexpl curexpl` from compcore.c:221.
4646pub static curexpl: OnceLock<Mutex<Option<Cexpl>>> = OnceLock::new(); // c:221
4647
4648/// Port of `LinkList matchers` from compcore.c:236. The C list holds
4649/// `Cmatcher` pointers (the parsed match-spec chains pushed by
4650/// addmatches's `add_bmatchers` block). Rust port mirrors that with
4651/// owned `Box<Cmatcher>` entries.
4652pub static matchers: OnceLock<Mutex<Vec<Box<crate::ported::zle::comp_h::Cmatcher>>>> =
4653    OnceLock::new(); // c:236
4654
4655/// Port of `mod_export Aminfo ainfo` from compcore.c:246.
4656pub static ainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
4657/// Port of `mod_export Aminfo fainfo` from compcore.c:246.
4658pub static fainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
4659
4660/// Port of `mod_export LinkList allccs` from compcore.c:259.
4661pub static allccs: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:259
4662
4663/// Port of `int fromcomp` from compcore.c:271.
4664pub static fromcomp: AtomicI32 = AtomicI32::new(0); // c:271
4665
4666/// Port of `mod_export int lastend` from compcore.c:276.
4667pub static lastend: AtomicI32 = AtomicI32::new(0); // c:276
4668
4669/// Port of `mod_export Brinfo brbeg` from `Src/Zle/zle_tricky.c`.
4670/// Linked list of opening-brace positions in the word being completed.
4671pub static BRBEG: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brbeg
4672
4673/// Port of `mod_export Brinfo brend` from `Src/Zle/zle_tricky.c`.
4674/// Linked list of closing-brace positions in the word being completed.
4675pub static BREND: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brend
4676
4677/// Port of `static int oldmenucmp` from compcore.c:457.
4678pub static OLDMENUCMP: AtomicI32 = AtomicI32::new(0); // c:457
4679
4680/// Port of `static int parwb` from compcore.c:540.
4681pub static PARWB: AtomicI32 = AtomicI32::new(0); // c:540
4682/// Port of `static int parwe` from compcore.c:540.
4683pub static PARWE: AtomicI32 = AtomicI32::new(0); // c:540
4684/// Port of `static int paroffs` from compcore.c:540.
4685pub static PAROFFS: AtomicI32 = AtomicI32::new(0); // c:540
4686
4687/// Port of `static int matchorder` from compcore.c:3169.
4688pub static MATCHORDER: AtomicI32 = AtomicI32::new(0); // c:3169
4689
4690/// Port of `mod_export int lastchar` from `Src/Zle/zle_main.c`. Last
4691/// keyboard char consumed by the binding loop — read by `selfinsert`.
4692pub static LASTCHAR: AtomicI32 = AtomicI32::new(0); // zle_main.c
4693                                                    // minfo_clear_cur / minfo_asked_zero deleted — Rust-only 2-line
4694                                                    // wrappers around C's inline writes `minfo.cur = NULL` and
4695                                                    // `minfo.asked = 0`. All call sites inlined.
4696
4697/// Direct port of `struct menuinfo minfo` — `Src/Zle/zle_tricky.c`
4698/// (the single file-scope instance). The struct type itself lives
4699/// in `comp_h.rs::Menuinfo` (port of comp.h:284-295).
4700pub static MINFO: OnceLock<Mutex<Menuinfo>> = OnceLock::new(); // zle_tricky.c minfo
4701
4702// =====================================================================
4703// matcheq — `Src/Zle/compcore.c:3203-3215`.
4704// =====================================================================
4705
4706#[inline]
4707fn matchstreq(a: Option<&String>, b: Option<&String>) -> bool {
4708    // c:3207
4709    match (a, b) {
4710        (None, None) => true,
4711        (Some(x), Some(y)) => x == y,
4712        _ => false,
4713    }
4714}
4715
4716/// First-match shortcut path from compcore.c:398-411. `Cmgroup m = amatches;
4717/// while (!m->mcount) m = m->next; do_single(m->matches[0])`.
4718fn do_single_first_match() {
4719    // c:398
4720    let groups = amatches
4721        .get_or_init(|| Mutex::new(Vec::new()))
4722        .lock()
4723        .ok()
4724        .map(|g| g.clone())
4725        .unwrap_or_default();
4726    let first = groups
4727        .into_iter()
4728        .find(|g| g.mcount > 0)
4729        .and_then(|g| g.matches.first().cloned());
4730    if let Some(m) = first {
4731        // c:407 — `minfo.cur = NULL`.
4732        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4733            g.cur = None;
4734            g.asked = 0; // c:408
4735        }
4736        // c:409 — `do_single(m->matches[0])`. This is the actual
4737        // single-match insert: it deletes the word between wb/we (incl.
4738        // the addx placeholder) and inserts the completed string with
4739        // its suffix. Previously this call was omitted (the match was
4740        // merely stashed on minfo.cur), so a unique completion left the
4741        // typed prefix + 'x' on the line instead of the completed word.
4742        crate::ported::zle::compresult::do_single(&m);
4743    }
4744}
4745
4746/// compcore.c:444 `compend:` epilogue — free matchers, snap zlemetacs.
4747fn goto_compend(ret: i32) -> i32 {
4748    // c:444
4749    if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
4750        g.clear(); // c:445-446 freecmatcher loop
4751    }
4752    let line_len = ZLEMETALL.load(Ordering::Relaxed); // c:448 strlen(zlemetaline)
4753    if ZLEMETACS.load(Ordering::Relaxed) > line_len {
4754        // c:449
4755        ZLEMETACS.store(line_len, Ordering::Relaxed); // c:450
4756    }
4757    ret // c:453
4758}
4759
4760// `COMP_LIST_COMPLETE` / `QT_NONE_STUB` / `QT_BACKSLASH_STUB` local
4761// aliases deleted — call sites now reach the real C-side constants
4762// directly (`crate::ported::zle::zle_h::COMP_LIST_COMPLETE`,
4763// `QT_NONE`, `QT_BACKSLASH`).
4764// The local `COMP_LIST_COMPLETE = 2` was a value-mismatch bug (the
4765// real constant is 1 per `Src/Zle/zle.h:357`).
4766
4767// `char_from_qt` deleted — Rust-only 1-line `(qt as u8) as char`
4768// helper. Inlined at the two call sites in get_compstate_str.
4769
4770// `showinglist_stub` / `showinglist_set` / `clearlist_set` /
4771// `listshown_stub` / `instring_stub` deleted — Rust-only 1-line
4772// accessors for C globals (SHOWINGLIST / CLEARLIST / LISTSHOWN /
4773// INSTRING). C reads/writes the bare globals inline; callers in
4774// compcore.rs now do `<GLOBAL>.load(Ordering::Relaxed)` /
4775// `<GLOBAL>.store(v, Ordering::Relaxed)` directly.
4776// `fn foredel` / `fn inststr` locals deleted — both duplicated
4777// canonical ports living in their proper home files:
4778//   - foredel: `Src/Zle/zle_utils.c:1105`
4779//     → `foredel`
4780//   - inststr: macro `Src/Zle/zle_tricky.c:57` (inststr(X) →
4781//     inststrlen(X,1,-1)) → `inststr`
4782// The duplicates here narrowed C signatures (foredel dropped `flags`,
4783// inststr dropped the i32 return + duplicated inststrlen's body) and
4784// violated Rule C (every decl in its mirroring C file). Callers in
4785// this module now route through the canonical ported.
4786/// `IN_NOTHING_LW` constant.
4787// These MUST match the raw IN_* enum in zsh_h (Src/zsh.h:2322-2332), which is
4788// the single encoding C uses for both `inwhat` and `linwhat`. `linwhat` is
4789// copied verbatim from `INWHAT` (makecomplist c:960), so a divergent ordering
4790// here made makecomplistglobal's `linwhat == IN_ENV_LW` never match after an
4791// assignment (`x=<Tab>`): INWHAT held IN_ENV=4 while IN_ENV_LW was 5, so
4792// value/assignment completion silently did nothing.
4793pub const IN_NOTHING_LW: i32 = 0; // = IN_NOTHING (zsh.h:2322)
4794/// `IN_CMD_LW` constant.
4795pub const IN_CMD_LW: i32 = 1; // = IN_CMD (zsh.h:2324)
4796/// `IN_MATH_LW` constant.
4797pub const IN_MATH_LW: i32 = 2; // = IN_MATH (zsh.h:2326)
4798/// `IN_COND_LW` constant.
4799pub const IN_COND_LW: i32 = 3; // = IN_COND (zsh.h:2328)
4800/// `IN_ENV_LW` constant.
4801pub const IN_ENV_LW: i32 = 4; // = IN_ENV (zsh.h:2330)
4802/// `IN_PAR_LW` constant.
4803pub const IN_PAR_LW: i32 = 5; // = IN_PAR (zsh.h:2332)
4804                              // `origline_stub` / `origcs_stub` deleted — Rust-only 1-line
4805                              // accessors for the `ORIGLINE` / `ORIGCS` globals (ports of C
4806                              // `origline` / `origcs` at zle_tricky.c:75 etc.). C reads these
4807                              // globals inline; callers in compcore.rs now do the lock/load
4808                              // directly.
4809/// Port of `void unmetafy_line(void)` from `zle_tricky.c:995`.
4810///
4811/// C body:
4812///   `zlemetaline[zlemetall] = '\0';
4813///    zleline = stringaszleline(zlemetaline, zlemetacs, &zlell,
4814///                              &linesz, &zlecs);
4815///    free(zlemetaline); zlemetaline = NULL;
4816///    CCRIGHT();`
4817///
4818/// Reads ZLEMETALINE, decodes via the canonical stringaszleline
4819/// (handles incs adjustment + unmetafy + UTF-8 decode), populates
4820/// ZLELINE / ZLELL / ZLECS, clears ZLEMETALINE/ZLEMETALL.
4821pub fn unmetafy_line() {
4822    // zle_tricky.c:995
4823    let meta = ZLEMETALINE
4824        .get_or_init(|| Mutex::new(String::new()))
4825        .lock()
4826        .map(|g| g.clone())
4827        .unwrap_or_default();
4828    let zlemetacs = ZLEMETACS.load(Ordering::Relaxed) as i32;
4829    let mut out_ll: i32 = 0;
4830    let mut out_cs: i32 = 0;
4831    // c:998 — `zleline = stringaszleline(zlemetaline, zlemetacs, &zlell, &linesz, &zlecs);`
4832    let line = crate::ported::zle::zle_utils::stringaszleline(
4833        &meta,
4834        zlemetacs,
4835        Some(&mut out_ll),
4836        None,
4837        Some(&mut out_cs),
4838    );
4839    if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
4840        *g = line.iter().collect();
4841    }
4842    ZLELL.store(out_ll, Ordering::Relaxed);
4843    ZLECS.store(out_cs, Ordering::Relaxed);
4844    // c:1001-1002 — `free(zlemetaline); zlemetaline = NULL;`. Rust:
4845    // clear the buffer + zero the length to mark meta-mode inactive.
4846    if let Some(m) = ZLEMETALINE.get() {
4847        if let Ok(mut g) = m.lock() {
4848            g.clear();
4849        }
4850    }
4851    ZLEMETALL.store(0, Ordering::Relaxed);
4852    // c:1007 — CCRIGHT(): combining-char alignment fixup. No-op in
4853    // this Rust port (handled by stringaszleline's codepoint walk).
4854}
4855
4856/// Port of `void metafy_line(void)` from `zle_tricky.c:978`.
4857///
4858/// C body:
4859///   `zlemetaline = zlelineasstring(zleline, zlell, zlecs,
4860///                                  &zlemetall, &zlemetacs, 0);
4861///    metalinesz = zlemetall;
4862///    free(zleline); zleline = NULL;`
4863///
4864/// Reads ZLELINE, encodes via the canonical zlelineasstring (handles
4865/// wcrtomb + metafy expansion), populates ZLEMETALINE / ZLEMETALL /
4866/// ZLEMETACS, clears ZLELINE/ZLELL.
4867pub fn metafy_line() {
4868    // zle_tricky.c:978
4869    let raw_vec: Vec<char> = ZLELINE
4870        .get_or_init(|| Mutex::new(String::new()))
4871        .lock()
4872        .map(|g| g.chars().collect())
4873        .unwrap_or_default();
4874    let zlell = raw_vec.len();
4875    let zlecs = ZLECS.load(Ordering::Relaxed);
4876    let mut out_ll: i32 = 0;
4877    let mut out_cs: i32 = 0;
4878    // c:982 — `zlemetaline = zlelineasstring(zleline, zlell, zlecs, &zlemetall, &zlemetacs, 0);`
4879    let meta = crate::ported::zle::zle_utils::zlelineasstring(
4880        &raw_vec,
4881        zlell,
4882        zlecs,
4883        Some(&mut out_ll),
4884        Some(&mut out_cs),
4885        0,
4886    );
4887    if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
4888        *g = meta;
4889    }
4890    ZLEMETALL.store(out_ll, Ordering::Relaxed);
4891    ZLEMETACS.store(out_cs, Ordering::Relaxed);
4892    // c:985 — `metalinesz = zlemetall;`. Rust String grows on demand;
4893    // no separate sizeline tracker.
4894    // c:989-990 — `free(zleline); zleline = NULL;`. Rust: clear the
4895    // buffer + zero ZLELL.
4896    if let Ok(mut g) = ZLELINE.get().unwrap().lock() {
4897        g.clear();
4898    }
4899    ZLELL.store(0, Ordering::Relaxed);
4900}
4901
4902fn opt_isset(name: &str) -> i32 {
4903    // options.c
4904    if crate::ported::options::opt_state_get(name).unwrap_or(false) {
4905        1
4906    } else {
4907        0
4908    }
4909}
4910/// Real call into `getiparam(name)` — the canonical paramtab read.
4911/// Mirrors C's `getiparam` at params.c:3044 which reads the global
4912/// `paramtab` directly via `gethashnode2`.
4913fn env_iparam(name: &str) -> i32 {
4914    // params.c:3044
4915    crate::ported::params::getiparam(name) as i32
4916}
4917fn lastprebr_set(s: &str) {
4918    // zle_tricky.c lastprebr
4919    if let Ok(mut g) = LASTPREBR.get_or_init(|| Mutex::new(String::new())).lock() {
4920        *g = s.to_string();
4921    }
4922}
4923fn lastpostbr_set(s: &str) {
4924    // zle_tricky.c lastpostbr
4925    if let Ok(mut g) = LASTPOSTBR.get_or_init(|| Mutex::new(String::new())).lock() {
4926        *g = s.to_string();
4927    }
4928}
4929
4930/// Choose `$compstate[context]` per the lex classification in `inwhat`
4931/// (and the `ispar` modifier). Direct lift of compcore.c:591-617.
4932fn compcontext_for(_s: &str) -> String {
4933    // c:591
4934    let ip = ispar.load(Ordering::Relaxed); // c:599
4935    if ip == 2 {
4936        return "brace_parameter".into();
4937    } // c:600
4938    if ip == 1 {
4939        return "parameter".into();
4940    } // c:601
4941    let lw = linwhat.load(Ordering::Relaxed); // c:602
4942    match lw {
4943        // c:602
4944        x if x == IN_PAR_LW => "assign_parameter".into(), // c:603
4945        x if x == IN_MATH_LW => "math".into(),            // c:604-611
4946        x if x == IN_COND_LW => "condition".into(),       // c:613
4947        x if x == IN_ENV_LW => "value".into(),            // c:615
4948        _ => "command".into(),                            // c:617
4949    }
4950}
4951
4952/// File-scope `int offs` from `Src/Zle/zle_tricky.c:88`. The C source
4953/// declares this as `mod_export`; mirrored here per Rule 9 since it's
4954/// not yet at a canonical Rust home.
4955pub static OFFS: AtomicI32 = AtomicI32::new(0); // zle_tricky.c:88
4956
4957/// File-scope `Compctl freecl` from `Src/Zle/compcore.c:255`. The
4958/// freelist of available Compctl slots for the current completion call.
4959pub static freecl: OnceLock<Mutex<Option<i32>>> = OnceLock::new(); // c:255
4960
4961/// Real call into `doshfunc` — `Src/exec.c`. Looks up the function
4962/// in the global shfunctab (`getshfunc`) and dispatches via the VM's
4963/// `functions_compiled` map. Returns the function's exit status
4964/// (LASTVAL after the call), matching C's `doshfunc` return value.
4965pub fn shfunc_call(name: &str) -> i32 {
4966    // exec.c
4967    if crate::ported::utils::getshfunc(name).is_none() {
4968        // c:exec.c:5800
4969        return 1; // missing fn → status 1
4970    }
4971    // Route through the canonical exec accessors dispatcher so the
4972    // function actually executes. Hook returns Option<i32>; None
4973    // means no executor context is set up yet (fall back to
4974    // LASTVAL read).
4975    crate::ported::exec::dispatch_function_call(name, &[])
4976        .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
4977}
4978/// Real call into `setsparam(&format!("compstate[{key}]"), val)` — the
4979/// canonical paramtab write. Mirrors C's `setsparam` at params.c:3350.
4980///
4981/// Now `pub` so compsys engine ports can write `$compstate[KEY]`
4982/// directly. Also dual-writes to `paramtab_hashed_storage()` under
4983/// the "compstate" key so subscript lookups via the hash-param
4984/// machinery see the same value — `$compstate` IS a PM_HASHED param
4985/// (created by `makecompparams` at `complete.rs:1499`), and shell
4986/// scripts read it as such.
4987pub fn set_compstate_str(key: &str, val: &str) {
4988    // params.c:3350 — flat bracketed-param write (preserves the
4989    // pre-existing access path used by `set_compstate_str` callers).
4990    let pname = format!("compstate[{}]", key);
4991    let _ = setsparam(&pname, val);
4992
4993    // Hash-storage write: dual-store under the `compstate` hash so
4994    // `${compstate[KEY]}` shell reads (via the hashparam machinery)
4995    // and any direct `paramtab_hashed_storage()` consumer see the
4996    // same value.
4997    if let Ok(mut tab) = paramtab_hashed_storage().lock() {
4998        tab.entry("compstate".to_string())
4999            .or_default()
5000            .insert(key.to_string(), val.to_string());
5001    }
5002}
5003
5004/// Read `$compstate[KEY]`. Returns `None` when the key was never set.
5005///
5006/// Prefers the hash-storage view (the canonical home for a PM_HASHED
5007/// param); falls back to the legacy flat `compstate[KEY]` bracketed
5008/// param for entries that some code wrote via raw `setsparam` without
5009/// going through [`set_compstate_str`].
5010pub fn get_compstate_str(key: &str) -> Option<String> {
5011    // c:complete.c:1411-1414 — `compstate[nmatches]` is a LIVE GSU integer:
5012    // `get_nmatches` flushes pending match groups via `permmatches(0)` and
5013    // returns the running `nmatches` counter. The stored-hash read below
5014    // served a stale 0 for it, so every completer's `nm != $compstate[nmatches]`
5015    // idiom (_describe, _arguments, _alternative, …) concluded "nothing was
5016    // added" and option completion died even though addmatches had added
5017    // hundreds of matches.
5018    if key == "nmatches" {
5019        let v = if permmatches(0) != 0 {
5020            0
5021        } else {
5022            nmatches.load(Ordering::Relaxed)
5023        };
5024        return Some(v.to_string());
5025    }
5026    if let Ok(tab) = paramtab_hashed_storage().lock() {
5027        if let Some(hash) = tab.get("compstate") {
5028            if let Some(v) = hash.get(key) {
5029                return Some(v.clone());
5030            }
5031        }
5032    }
5033    // Fallback: pre-existing callers wrote via raw `setsparam` only.
5034    let pname = format!("compstate[{}]", key);
5035    getsparam(&pname)
5036}
5037
5038/// Local helper: position before-the-current char (handles UTF-8).
5039#[inline]
5040fn prev_char_index(bytes: &[u8], pos: usize) -> usize {
5041    // local
5042    if pos == 0 {
5043        return 0;
5044    }
5045    let mut i = pos - 1;
5046    while i > 0 && (bytes[i] & 0xC0) == 0x80 {
5047        i -= 1;
5048    }
5049    i
5050}
5051
5052#[inline]
5053fn char_at(bytes: &[u8], pos: usize) -> char {
5054    // local
5055    if pos >= bytes.len() {
5056        return '\0';
5057    }
5058    let s = match std::str::from_utf8(&bytes[pos..]) {
5059        Ok(s) => s,
5060        Err(_) => return '\0',
5061    };
5062    s.chars().next().unwrap_or('\0')
5063}
5064
5065/// Depth counter so `set_comp_sep`'s sanity assert ("lexsave/restore
5066/// balanced") fires when a future port mismatches them.
5067static LEXSAVE_DEPTH: AtomicI32 = AtomicI32::new(0); // local
5068
5069/// Walk a balanced pair of in/out token bytes starting at `start`,
5070/// returning the index just after the closing token, or None if
5071/// unbalanced. C `skipparens` returns the position; this version
5072/// returns the same semantic.
5073fn skip_token_parens(bytes: &[u8], start: usize, open: char, close: char) -> Option<usize> {
5074    let mut depth: i32 = 0;
5075    let mut i = start;
5076    while i < bytes.len() {
5077        let c = char_at(bytes, i);
5078        if c == open {
5079            depth += 1;
5080        } else if c == close {
5081            depth -= 1;
5082            if depth == 0 {
5083                return Some(i + c.len_utf8());
5084            }
5085        }
5086        i += c.len_utf8();
5087    }
5088    if depth == 0 {
5089        Some(i)
5090    } else {
5091        None
5092    }
5093}
5094
5095/// Walk the INAMESPC name-character class — equivalent to C's
5096/// `itype_end(e, INAMESPC, 0)` loop. Stops at first non-name char.
5097fn walk_namespace(bytes: &[u8]) -> usize {
5098    // local
5099    let s = match std::str::from_utf8(bytes) {
5100        Ok(s) => s,
5101        Err(_) => return 0,
5102    };
5103    let mut len = 0usize;
5104    for c in s.chars() {
5105        if c.is_alphanumeric() || c == '_' {
5106            len += c.len_utf8();
5107        } else {
5108            break;
5109        }
5110    }
5111    len
5112}
5113
5114/// Strip Inbrace/Outbrace/Stringg/etc. token bytes back to literal
5115/// characters — substitute for C `untokenize()` over the slice. The
5116/// canonical Rust untokenize lives in `crate::lex::untokenize`.
5117fn strip_tokens(s: &str) -> String {
5118    // local
5119    crate::lex::untokenize(s).to_string()
5120}
5121
5122/// File-scope `int hcompcall` accessor — `compfunc` active iff non-empty.
5123fn compfunc_active() -> bool {
5124    compfunc
5125        .get_or_init(|| Mutex::new(None))
5126        .lock()
5127        .ok()
5128        .and_then(|g| g.clone())
5129        .map(|s| !s.is_empty())
5130        .unwrap_or(false)
5131}
5132
5133/// Direct port of `void lexsave(void)` from `Src/lex.c`. Delegates
5134/// to `zcontext_save` which pushes the lex/parse/hist context stack
5135/// frame. Returns a token (current stack depth) for symmetry with
5136/// the C `int` save token used by `set_comp_sep` for invariant check.
5137fn lexsave() -> usize {
5138    // lex.c via context.c:80
5139    crate::ported::context::zcontext_save();
5140    (LEXSAVE_DEPTH.fetch_add(1, Ordering::SeqCst) + 1) as usize
5141}
5142
5143/// Direct port of `void lexrestore(void)` from `Src/lex.c`. Pops the
5144/// last `zcontext_save` frame. C body restores hist/lex/parse via
5145/// `zcontext_restore_partial(ZCONTEXT_HIST|ZCONTEXT_LEX|ZCONTEXT_PARSE)`.
5146fn lexrestore(_token: usize) {
5147    // lex.c via context.c:117
5148    let parts = ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE;
5149    zcontext_restore_partial(parts);
5150    LEXSAVE_DEPTH.fetch_sub(1, Ordering::SeqCst);
5151}
5152
5153// ---- Extern stubs for addmatches's bucket-3 dependencies ----
5154
5155fn compquote_first() -> Option<char> {
5156    // zle_tricky.c compquote
5157    COMPQUOTE
5158        .get_or_init(|| Mutex::new(String::new()))
5159        .lock()
5160        .ok()
5161        .and_then(|g| g.chars().next())
5162}
5163fn instring_set(v: i32) {
5164    // zle_tricky.c:419
5165    INSTRING.store(v, Ordering::Relaxed);
5166}
5167fn inbackt_set(v: i32) {
5168    // zle_tricky.c:419
5169    INBACKT.store(v, Ordering::Relaxed);
5170}
5171fn autoq_set(s: &str) {
5172    // zle_tricky.c autoq
5173    if let Ok(mut g) = AUTOQ.get_or_init(|| Mutex::new(String::new())).lock() {
5174        *g = s.to_string();
5175    }
5176}
5177
5178// ---- Extern stubs for makecomplist's bucket-3 dependencies ----
5179
5180/// File-scope holder for `Cmlist bmatchers` — `Src/Zle/compcore.c:236`.
5181/// C linked-list of matchers active for brace-matching, populated by
5182/// `add_bmatchers` walking the user-installed `Cmatcher` chain.
5183pub static bmatchers: OnceLock<Mutex<Option<Box<Cmlist>>>> = OnceLock::new(); // c:236
5184
5185/// File-scope holder for `Cmlist mstack` — `Src/Zle/compcore.c:236`.
5186/// Matcher-stack — current active matcher list for compadd recursion.
5187pub static mstack: OnceLock<Mutex<Option<Box<Cmlist>>>> = OnceLock::new(); // c:236
5188
5189// ---- Extern stubs for add_match_data's Cline operations ----
5190
5191/// Bridge to `cline_matched()` — `Src/Zle/compmatch.c:253`. The
5192/// real port takes `&mut Option<Box<Cline>>` walking the chain
5193/// marking each node CLF_MATCHED. With only a string slice here we
5194/// build a one-node Cline shim and route the call through it so the
5195/// CLF_MATCHED state-machine update fires the same way as in C.
5196fn cline_matched_compcore(line: Option<&str>) {
5197    // compmatch.c:253
5198    let Some(s) = line else {
5199        return;
5200    };
5201    if s.is_empty() {
5202        return;
5203    }
5204    let mut head = Some(Box::new(Cline {
5205        line: Some(s.to_string()),
5206        llen: s.len() as i32,
5207        ..Default::default()
5208    }));
5209    cline_matched(&mut head);
5210}
5211/// Real read of `char *qisuf` via the paramtab. Mirrors C's direct
5212/// global read at `Src/Zle/zle_tricky.c qisuf`.
5213fn qisuf_get() -> String {
5214    // zle_tricky.c qisuf
5215    getsparam("qisuf").unwrap_or_default()
5216}
5217fn qipre_get() -> String {
5218    // zle_tricky.c qipre
5219    getsparam("qipre").unwrap_or_default()
5220}
5221
5222/// Adapter for `int movefd(int fd)` from `Src/utils.c:2974` —
5223/// delegates to the canonical port in `ported::utils::movefd`.
5224fn movefd(fd: i32) -> i32 {
5225    // utils.c:2974
5226    crate::ported::utils::movefd(fd)
5227}
5228
5229/// Adapter for `void redup(int new, int old)` from `Src/utils.c:2021` —
5230/// delegates to the canonical port `ported::utils::redup`. Callers
5231/// only need the new-fd form here; `old` is the inverse of movefd's
5232/// reservation (passed as -1 to mean "no original").
5233fn redup(new: i32) {
5234    // utils.c:2021
5235    crate::ported::utils::redup(new, -1);
5236}
5237
5238/// File-scope registry mirroring `Src/init.c`'s `zshhooks[]` table —
5239/// each hook name maps to the ordered list of shfunc names to call.
5240pub static HOOK_FNS: OnceLock<Mutex<std::collections::HashMap<String, Vec<String>>>> =
5241    OnceLock::new(); // init.c zshhooks
5242
5243/// Adapter for the `errflag` global from `Src/init.c` — reads the
5244/// canonical atomic in `ported::utils::errflag`.
5245fn errflag_get() -> bool {
5246    crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 // init.c
5247}
5248
5249/// Local dispatcher used by compcore call sites for hook names that
5250/// don't yet have a typed-data argument. Delegates to the canonical
5251/// `module::runhookdef(gethookdef(name), NULL)` — no-op when no
5252/// Hookfn is registered (c:993-995). Returns the Hookfn return value
5253/// (or 0 when no handler fires).
5254fn runhookdef_compcore(hook: &str) -> i32 {
5255    // c:990
5256    let h = gethookdef(hook);
5257    if h.is_null() {
5258        return 0;
5259    }
5260    runhookdef(h, std::ptr::null_mut())
5261}
5262
5263/// Port of `static int ccmakehookfn(Hookdef, Ccmakedat dat)` from
5264/// `Src/Zle/compctl.c:1763` — the function the compctl module registers
5265/// on COMPCTLMAKEHOOK. This is the compctl-side analog of the compfunc
5266/// branch in `makecomplist`: it builds the match list via
5267/// `makecomplistglobal` (the default command/file completion the bare
5268/// `zsh -f` shell uses), finalizes it with `permmatches`, and reports
5269/// success through `dat.lst` (0 = matches, 1 = none). The previous stub
5270/// only called the unrelated `makecomplistctl` and never set `dat.lst`,
5271/// so `makecomplist` returned the raw list-type (nonzero) and
5272/// `do_completion` always took the feep/error path — `l<Tab>` produced
5273/// nothing.
5274///
5275/// The C source loops over the global matcher list (`cmatcher`); the
5276/// common case (and always under `-f`) has none, so this port runs the
5277/// single no-matcher pass, matching begcmgroup/makecomplistglobal/
5278/// endcmgroup/permmatches exactly as the compfunc branch does.
5279fn runhookdef_compctlmake(
5280    // c:1763
5281    dat: &mut Ccmakedat,
5282) {
5283    let os = dat.str.clone().unwrap_or_default();
5284    let incmd = dat.incmd;
5285    let lst = dat.lst;
5286
5287    // c:1794-1810 — no global matchers → mstack = NULL, bmatchers = NULL.
5288    if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
5289        *g = None;
5290    }
5291    if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
5292        *g = None;
5293    }
5294    // c:1812-1813 — ainfo = fainfo = hcalloc.
5295    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
5296        *g = Some(Aminfo::default());
5297    }
5298    if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
5299        *g = Some(Aminfo::default());
5300    }
5301    if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
5302        *g = None; // c:1815
5303    }
5304    if VALIDLIST.load(Ordering::Relaxed) == 0 {
5305        LASTAMBIG.store(0, Ordering::Relaxed); // c:1817
5306    }
5307    if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5308        g.clear(); // c:1818
5309    }
5310    mnum.store(0, Ordering::Relaxed); // c:1819
5311    unambig_mnum.store(-1, Ordering::Relaxed); // c:1820
5312    if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
5313        g.clear(); // c:1821
5314    }
5315    insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed); // c:1822
5316    oldlist.store(0, Ordering::Relaxed); // c:1829
5317    oldins.store(0, Ordering::Relaxed); // c:1829
5318    begcmgroup(Some("default"), 0); // c:1830
5319    MENUCMP.store(0, Ordering::Relaxed); // c:1831
5320    menuacc.store(0, Ordering::Relaxed); // c:1831
5321    newmatches.store(0, Ordering::Relaxed); // c:1831
5322    onlyexpl.store(0, Ordering::Relaxed); // c:1831
5323
5324    // c:1836-1837 — makecomplistglobal(s, incmd, lst, 0) generates the
5325    // matches (per-command compctl or the default command/file logic).
5326    crate::ported::zle::compctl::makecomplistglobal(&os, incmd != 0, lst, 0);
5327    endcmgroup(None); // c:1838
5328
5329    // c:1879-1892 — permmatches(1); amatches = pmatches; swap holders.
5330    permmatches(1); // c:1879
5331    let p_snap = pmatches
5332        .get_or_init(|| Mutex::new(Vec::new()))
5333        .lock()
5334        .ok()
5335        .map(|g| g.clone())
5336        .unwrap_or_default();
5337    if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5338        *g = p_snap.clone(); // c:1880
5339    }
5340    lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1881
5341    lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1882
5342    if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5343        *g = p_snap; // c:1884
5344    }
5345    let lm_snap = lmatches
5346        .get_or_init(|| Mutex::new(None))
5347        .lock()
5348        .ok()
5349        .and_then(|g| g.clone());
5350    if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
5351        *g = lm_snap; // c:1885
5352    }
5353    if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5354        g.clear(); // c:1886
5355    }
5356    hasperm.store(0, Ordering::Relaxed); // c:1887
5357    hasoldlist.store(1, Ordering::Relaxed); // c:1888
5358
5359    // c:1890-1901 — success iff we produced matches and no error.
5360    if nmatches.load(Ordering::Relaxed) != 0 && !errflag_get() {
5361        VALIDLIST.store(1, Ordering::Relaxed); // c:1891
5362        dat.lst = 0; // c:1894
5363    } else {
5364        dat.lst = 1; // c:1908
5365    }
5366}
5367
5368// =====================================================================
5369// permmatches — `Src/Zle/compcore.c:3423`.
5370// =====================================================================
5371
5372/// Static state for `permmatches`'s `static int fi`. C scopes the
5373/// flag to the function; Rust hoists it to file scope per Rule S1.
5374static PERMMATCHES_FI: AtomicI32 = AtomicI32::new(0); // c:3423 static int fi
5375
5376/// Port of the `type==0` string-sort branch of `makearray()` from
5377/// compcore.c:3239-3257. Sorts strings via `strmetasort` + dedup.
5378pub fn makearray_strings(mut rp: Vec<String>, flags: i32) -> (Vec<String>, i32) {
5379    // c:3239
5380    let mut n: i32 = rp.len() as i32;
5381    if flags != 0 && n > 0 {
5382        // c:3240
5383        let numeric = isset(NUMERICGLOBSORT); // c:3243
5384        let mut sf = SORTIT_IGNORING_BACKSLASHES as u32;
5385        if numeric {
5386            sf |= SORTIT_NUMERICALLY as u32;
5387        }
5388        crate::ported::sort::strmetasort(&mut rp, sf, None); // c:3242-3244
5389
5390        // Dedup consecutive equals.                                         // c:3247
5391        let mut cp = 0usize;
5392        let mut ap = 0usize;
5393        while ap < rp.len() {
5394            if ap != cp {
5395                rp.swap(ap, cp);
5396            }
5397            cp += 1;
5398            let mut bp = ap;
5399            while bp + 1 < rp.len() && rp[ap] == rp[bp + 1] {
5400                // c:3250
5401                bp += 1;
5402                n -= 1;
5403            }
5404            ap = bp + 1; // c:3252
5405        }
5406        rp.truncate(cp); // c:3253
5407    }
5408    (rp, n)
5409}
5410
5411#[cfg(test)]
5412mod tests {
5413    use super::*;
5414
5415    #[test]
5416    fn rembslash_basic() {
5417        let _g = crate::test_util::global_state_lock();
5418        let _g = zle_test_setup();
5419        assert_eq!(rembslash("hello\\ world"), "hello world");
5420        assert_eq!(rembslash("no\\\\slash"), "no\\slash");
5421        assert_eq!(rembslash("plain"), "plain");
5422    }
5423
5424    #[test]
5425    fn comp_quoting_string_table() {
5426        let _g = crate::test_util::global_state_lock();
5427        let _g = zle_test_setup();
5428        assert_eq!(comp_quoting_string(QT_SINGLE), "'");
5429        assert_eq!(comp_quoting_string(QT_DOUBLE), "\"");
5430        assert_eq!(comp_quoting_string(QT_DOLLARS), "$'");
5431        assert_eq!(comp_quoting_string(0), "\\");
5432        assert_eq!(comp_quoting_string(QT_BACKSLASH), "\\");
5433    }
5434
5435    #[test]
5436    fn matcheq_equal_strings() {
5437        let _g = crate::test_util::global_state_lock();
5438        let _g = zle_test_setup();
5439        let mut a = Cmatch::default();
5440        a.str = Some("foo".into());
5441        let mut b = Cmatch::default();
5442        b.str = Some("foo".into());
5443        assert!(matcheq(&a, &b));
5444    }
5445
5446    #[test]
5447    fn matcheq_different_strings() {
5448        let _g = crate::test_util::global_state_lock();
5449        let _g = zle_test_setup();
5450        let mut a = Cmatch::default();
5451        a.str = Some("foo".into());
5452        let mut b = Cmatch::default();
5453        b.str = Some("bar".into());
5454        assert!(!matcheq(&a, &b));
5455    }
5456
5457    #[test]
5458    fn matcheq_one_side_none() {
5459        let _g = crate::test_util::global_state_lock();
5460        let _g = zle_test_setup();
5461        let mut a = Cmatch::default();
5462        a.pre = Some("p".into());
5463        let b = Cmatch::default();
5464        assert!(!matcheq(&a, &b));
5465    }
5466
5467    #[test]
5468    fn get_user_var_reads_array_from_paramtab() {
5469        let _g = crate::test_util::global_state_lock();
5470        // c:2003 — `getaparam(nam)` first. Verify array params come
5471        //          out as a Vec, not via env.
5472        let _g = zle_test_setup();
5473        setaparam("__test_arr", vec!["a".into(), "bb".into(), "ccc".into()]);
5474        let got = get_user_var(Some("__test_arr"));
5475        assert_eq!(got, Some(vec!["a".into(), "bb".into(), "ccc".into()]));
5476        // Cleanup so we don't poison other tests.
5477        setaparam("__test_arr", vec![]);
5478    }
5479
5480    #[test]
5481    fn get_user_var_reads_scalar_as_single_element_array() {
5482        let _g = crate::test_util::global_state_lock();
5483        // c:2007-2009 — getsparam fallback: wrap scalar in 1-element array.
5484        let _g = zle_test_setup();
5485        setsparam("__test_scalar", "hello");
5486        let got = get_user_var(Some("__test_scalar"));
5487        assert_eq!(got, Some(vec!["hello".to_string()]));
5488        setsparam("__test_scalar", "");
5489    }
5490
5491    #[test]
5492    fn get_user_var_paren_list_splits_on_separators() {
5493        let _g = crate::test_util::global_state_lock();
5494        // c:1960-1996 — `(a b c)` paren list, NOT a param lookup.
5495        let _g = zle_test_setup();
5496        let got = get_user_var(Some("(one two three)"));
5497        assert_eq!(got, Some(vec!["one".into(), "two".into(), "three".into()]));
5498    }
5499
5500    #[test]
5501    fn get_user_var_none_for_missing() {
5502        let _g = crate::test_util::global_state_lock();
5503        // c:1956 + c:2009 — missing param returns None.
5504        let _g = zle_test_setup();
5505        // (env vars must not leak through — we don't read $PATH etc.)
5506        let got = get_user_var(Some("__definitely_not_a_param_xyz"));
5507        assert_eq!(got, None);
5508    }
5509
5510    #[test]
5511    fn get_data_arr_reads_hashed_keys_or_values() {
5512        let _g = crate::test_util::global_state_lock();
5513        // c:2022 — fetchvalue(name, SCANPM_WANTKEYS|WANTVALS|MATCHMANY).
5514        let _g = zle_test_setup();
5515        crate::ported::params::sethparam(
5516            "__test_hash",
5517            vec!["k1".into(), "v1".into(), "k2".into(), "v2".into()],
5518        );
5519
5520        let keys = get_data_arr("__test_hash", true);
5521        assert!(keys.is_some(), "hashed param should have keys");
5522        let mut keys = keys.unwrap();
5523        keys.sort();
5524        assert_eq!(keys, vec!["k1".to_string(), "k2".to_string()]);
5525
5526        let vals = get_data_arr("__test_hash", false);
5527        assert!(vals.is_some(), "hashed param should have values");
5528        let mut vals = vals.unwrap();
5529        vals.sort();
5530        assert_eq!(vals, vec!["v1".to_string(), "v2".to_string()]);
5531    }
5532
5533    #[test]
5534    fn get_data_arr_none_for_non_hashed() {
5535        let _g = crate::test_util::global_state_lock();
5536        // c:2032 — fetchvalue NULL → return NULL for params that
5537        //          aren't associative arrays.
5538        let _g = zle_test_setup();
5539        setsparam("__test_scalar2", "value");
5540        let got = get_data_arr("__test_scalar2", false);
5541        assert_eq!(got, None, "scalar params must NOT come out of get_data_arr");
5542    }
5543
5544    #[test]
5545    fn before_complete_snapshots_oldmenucmp() {
5546        let _g = crate::test_util::global_state_lock();
5547        // c:463 — `oldmenucmp = menucmp;`
5548        let _g = zle_test_setup();
5549        MENUCMP.store(7, Ordering::Relaxed);
5550        OLDMENUCMP.store(0, Ordering::Relaxed);
5551        let mut lst = 0;
5552        let _ = before_complete(&mut lst);
5553        assert_eq!(OLDMENUCMP.load(Ordering::Relaxed), 7);
5554        // Reset for other tests.
5555        MENUCMP.store(0, Ordering::Relaxed);
5556        OLDMENUCMP.store(0, Ordering::Relaxed);
5557    }
5558
5559    #[test]
5560    fn before_complete_clears_showagain() {
5561        let _g = crate::test_util::global_state_lock();
5562        // c:467 — `showagain = 0;` always (after the validlist gate).
5563        let _g = zle_test_setup();
5564        SHOWAGAIN.store(5, Ordering::Relaxed);
5565        let mut lst = 0;
5566        let _ = before_complete(&mut lst);
5567        assert_eq!(
5568            SHOWAGAIN.load(Ordering::Relaxed),
5569            0,
5570            "SHOWAGAIN must be cleared by before_complete"
5571        );
5572    }
5573
5574    #[test]
5575    fn remsquote_default_quoting() {
5576        let _g = crate::test_util::global_state_lock();
5577        let _g = zle_test_setup();
5578        let mut s = String::from("a'\\''b");
5579        let n = remsquote(&mut s);
5580        assert_eq!(s, "a'b");
5581        assert_eq!(n, 3);
5582    }
5583
5584    #[test]
5585    fn ctokenize_dollar_substitution() {
5586        let _g = crate::test_util::global_state_lock();
5587        let _g = zle_test_setup();
5588        let out = ctokenize("$x{y}");
5589        let chars: Vec<char> = out.chars().collect();
5590        assert_eq!(chars[0], Stringg);
5591        assert_eq!(chars[1], 'x');
5592        assert_eq!(chars[2], Inbrace);
5593        assert_eq!(chars[3], 'y');
5594        assert_eq!(chars[4], Outbrace);
5595    }
5596
5597    #[test]
5598    fn get_user_var_inline_list() {
5599        let _g = crate::test_util::global_state_lock();
5600        let _g = zle_test_setup();
5601        let result = get_user_var(Some("(a b c)")).unwrap();
5602        assert_eq!(result, vec!["a", "b", "c"]);
5603    }
5604
5605    #[test]
5606    fn matchcmp_str_sort_default() {
5607        let _g = crate::test_util::global_state_lock();
5608        let _g = zle_test_setup();
5609        MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
5610        let mut a = Cmatch::default();
5611        a.str = Some("apple".into());
5612        let mut b = Cmatch::default();
5613        b.str = Some("banana".into());
5614        assert_eq!(matchcmp(&a, &b), std::cmp::Ordering::Less);
5615        assert_eq!(matchcmp(&b, &a), std::cmp::Ordering::Greater);
5616        assert_eq!(matchcmp(&a, &a), std::cmp::Ordering::Equal);
5617        MATCHORDER.store(0, Ordering::Relaxed);
5618    }
5619
5620    #[test]
5621    fn dupmatch_clones_strings_and_truncates_braces() {
5622        let _g = crate::test_util::global_state_lock();
5623        let _g = zle_test_setup();
5624        // C body c:3370: deep-copy strings, truncate brpl/brsl to nbeg/nend.
5625        let mut src = Cmatch::default();
5626        src.str = Some("foo".into());
5627        src.ipre = Some("ipre".into());
5628        src.flags = 7;
5629        src.brpl = vec![10, 20, 30, 40];
5630        src.brsl = vec![5, 6, 7];
5631        src.qipl = 1;
5632        src.qisl = 2;
5633        src.mode = 0o755;
5634        src.modec = 'd';
5635
5636        let r = dupmatch(&src, 2, 1);
5637        assert_eq!(r.str.as_deref(), Some("foo"));
5638        assert_eq!(r.ipre.as_deref(), Some("ipre"));
5639        assert_eq!(r.flags, 7);
5640        assert_eq!(r.brpl, vec![10, 20]); // truncated to nbeg=2
5641        assert_eq!(r.brsl, vec![5]); // truncated to nend=1
5642        assert_eq!(r.qipl, 1);
5643        assert_eq!(r.qisl, 2);
5644        assert_eq!(r.mode, 0o755);
5645        assert_eq!(r.modec, 'd');
5646    }
5647
5648    #[test]
5649    fn dupmatch_empty_braces_stay_empty() {
5650        let _g = crate::test_util::global_state_lock();
5651        let _g = zle_test_setup();
5652        // C body c:3395/3404: NULL brpl/brsl stay NULL regardless of nbeg/nend.
5653        let src = Cmatch::default();
5654        let r = dupmatch(&src, 5, 5);
5655        assert!(r.brpl.is_empty());
5656        assert!(r.brsl.is_empty());
5657    }
5658
5659    #[test]
5660    fn makearray_sorted_and_deduped() {
5661        let _g = crate::test_util::global_state_lock();
5662        let _g = zle_test_setup();
5663        // c:3262-3291: sort + dedup with matcheq. Same str + nil disp =>
5664        // collapses into one entry with CMF_FMULT set on the survivor.
5665        let mut a = Cmatch::default();
5666        a.str = Some("z".into());
5667        let mut b = Cmatch::default();
5668        b.str = Some("a".into());
5669        let mut c = Cmatch::default();
5670        c.str = Some("a".into());
5671        let (arr, n, _nl, _ll) = makearray(vec![a, b, c], CGF_MATSORT);
5672        // Two distinct visible strings after dedup ("a", "z").
5673        assert_eq!(arr.len(), 2);
5674        assert_eq!(n, 2);
5675        assert_eq!(arr[0].str.as_deref(), Some("a"));
5676        assert_eq!(arr[1].str.as_deref(), Some("z"));
5677    }
5678
5679    #[test]
5680    fn makearray_nosort_unchanged_order() {
5681        let _g = crate::test_util::global_state_lock();
5682        let _g = zle_test_setup();
5683        // c:3300: CGF_NOSORT branch; with no UNIQ flags, order preserved.
5684        let mut a = Cmatch::default();
5685        a.str = Some("z".into());
5686        let mut b = Cmatch::default();
5687        b.str = Some("a".into());
5688        let (arr, n, _, _) = makearray(vec![a, b], CGF_NOSORT | CGF_UNIQALL);
5689        // UNIQALL active so no dedup pass runs.
5690        assert_eq!(n, 2);
5691        assert_eq!(arr[0].str.as_deref(), Some("z"));
5692        assert_eq!(arr[1].str.as_deref(), Some("a"));
5693    }
5694
5695    #[test]
5696    fn makearray_strings_dedup_consecutive() {
5697        let _g = crate::test_util::global_state_lock();
5698        let _g = zle_test_setup();
5699        // c:3239 path: sort + drop adjacent duplicates.
5700        let (arr, n) = makearray_strings(vec!["b".into(), "a".into(), "a".into(), "c".into()], 1);
5701        assert_eq!(n, 3);
5702        assert_eq!(arr, vec!["a", "b", "c"]);
5703    }
5704
5705    #[test]
5706    fn check_param_no_dollar_returns_none() {
5707        let _g = crate::test_util::global_state_lock();
5708        let _g = zle_test_setup();
5709        // c:1316: no `$` in string → return None.
5710        OFFS.store(2, Ordering::Relaxed);
5711        assert_eq!(check_param("abc", false, false), None);
5712    }
5713
5714    #[test]
5715    fn check_param_simple_dollar_var_at_cursor() {
5716        let _g = crate::test_util::global_state_lock();
5717        let _g = zle_test_setup();
5718        // c:1259-1311: `$FOO` with cursor inside the name → return b.
5719        OFFS.store(2, Ordering::Relaxed);
5720        let s = format!("{}FOO", Stringg);
5721        let r = check_param(&s, false, true);
5722        assert!(r.is_some(), "expected Some(b) inside $FOO");
5723    }
5724
5725    #[test]
5726    fn callcompfunc_empty_fn_no_panic() {
5727        let _g = crate::test_util::global_state_lock();
5728        let _g = zle_test_setup();
5729        // c:552: getshfunc(NULL) early-return.
5730        callcompfunc("anything", "");
5731    }
5732
5733    #[test]
5734    fn callcompfunc_sets_compstate_context() {
5735        let _g = crate::test_util::global_state_lock();
5736        let _g = zle_test_setup();
5737        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5738        // c:619: context selection — verified via the pure
5739        // compcontext_for helper (callcompfunc calls it and writes
5740        // to paramtab via setsparam, but paramtab read-back in a
5741        // unit-test context without a live VM is unreliable).
5742        ispar.store(0, Ordering::Relaxed);
5743        linwhat.store(IN_PAR_LW, Ordering::Relaxed);
5744        assert_eq!(compcontext_for("foo"), "assign_parameter");
5745        // Body executes without panicking against the real paramtab.
5746        callcompfunc("foo", "_test_fn");
5747    }
5748
5749    /// Test-only serializer for tests that mutate file-scope globals.
5750    static GLOBAL_MUT_LOCK: Mutex<()> = Mutex::new(());
5751
5752    #[test]
5753    fn compcontext_for_routes_ispar_first() {
5754        let _g = crate::test_util::global_state_lock();
5755        let _g = zle_test_setup();
5756        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5757        ispar.store(2, Ordering::Relaxed);
5758        linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
5759        assert_eq!(compcontext_for("x"), "brace_parameter");
5760        ispar.store(1, Ordering::Relaxed);
5761        assert_eq!(compcontext_for("x"), "parameter");
5762        ispar.store(0, Ordering::Relaxed);
5763        linwhat.store(IN_MATH_LW, Ordering::Relaxed);
5764        assert_eq!(compcontext_for("x"), "math");
5765        linwhat.store(IN_COND_LW, Ordering::Relaxed);
5766        assert_eq!(compcontext_for("x"), "condition");
5767        linwhat.store(IN_ENV_LW, Ordering::Relaxed);
5768        assert_eq!(compcontext_for("x"), "value");
5769        linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
5770        assert_eq!(compcontext_for("x"), "command");
5771    }
5772
5773    #[test]
5774    fn addmatches_empty_argv_early_return() {
5775        let _g = crate::test_util::global_state_lock();
5776        let _g = zle_test_setup();
5777        // c:2138-2139: empty argv + dummies==0 + no CAF_ALL → return 1.
5778        let mut dat = Cadata::default();
5779        dat.dummies = 0;
5780        dat.aflags = 0;
5781        assert_eq!(addmatches(&mut dat, &[]), 1);
5782    }
5783
5784    #[test]
5785    fn addmatches_appends_argv_to_default_group() {
5786        let _g = crate::test_util::global_state_lock();
5787        let _g = zle_test_setup();
5788        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5789        // c:2200 simplified body: each argv entry → addmatch into "default" group.
5790        amatches
5791            .get_or_init(|| Mutex::new(Vec::new()))
5792            .lock()
5793            .unwrap()
5794            .clear();
5795        matches
5796            .get_or_init(|| Mutex::new(Vec::new()))
5797            .lock()
5798            .unwrap()
5799            .clear();
5800        let mut dat = Cadata::default();
5801        dat.dummies = -1;
5802        let _ = addmatches(&mut dat, &["a".into(), "b".into()]);
5803        let n = matches.get().unwrap().lock().unwrap().len();
5804        assert!(n >= 2);
5805    }
5806
5807    #[test]
5808    fn add_match_data_returns_populated_cmatch() {
5809        let _g = crate::test_util::global_state_lock();
5810        let _g = zle_test_setup();
5811        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5812        // c:3052-3067: cm.str/orig/pre/suf populated; mnum bumps by 1.
5813        matches
5814            .get_or_init(|| Mutex::new(Vec::new()))
5815            .lock()
5816            .unwrap()
5817            .clear();
5818        let before = mnum.load(Ordering::Relaxed);
5819        let cm = add_match_data(
5820            0,
5821            "match",
5822            "match-orig",
5823            None,
5824            "ipre",
5825            "ripre",
5826            "isuf",
5827            "pre",
5828            "prpre",
5829            "ppre",
5830            None,
5831            "psuf",
5832            None,
5833            "suf",
5834            0,
5835            0,
5836        );
5837        assert_eq!(cm.str.as_deref(), Some("match"));
5838        assert_eq!(cm.orig.as_deref(), Some("match-orig"));
5839        assert_eq!(cm.pre.as_deref(), Some("pre"));
5840        assert_eq!(cm.suf.as_deref(), Some("suf"));
5841        assert_eq!(mnum.load(Ordering::Relaxed), before + 1);
5842    }
5843
5844    #[test]
5845    fn add_match_data_exact_records_into_ainfo() {
5846        let _g = crate::test_util::global_state_lock();
5847        let _g = zle_test_setup();
5848        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5849        // c:3037-3058: exact != 0 writes `ai->exact = useexact` and
5850        // `ai->exactm = cm`. The test sets useexact=1 to exercise the
5851        // accept-exact path.
5852        let saved_useexact = useexact.load(Ordering::Relaxed);
5853        useexact.store(1, Ordering::Relaxed);
5854        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
5855            *g = Some(Aminfo::default());
5856        }
5857        let _ = add_match_data(
5858            0, "x", "x", None, "", "", "", "", "", "", None, "", None, "", 0, 1,
5859        );
5860        let a = ainfo.get().unwrap().lock().unwrap().clone().unwrap();
5861        useexact.store(saved_useexact, Ordering::Relaxed);
5862        assert_eq!(a.exact, 1);
5863        assert!(a.exactm.is_some());
5864    }
5865
5866    /// Faithful end-to-end exercise of `set_comp_sep` (`compset -q`): an
5867    /// ASCII argument `a b c` with the cursor at the end of the middle
5868    /// word must be re-lexed into three words, narrowed to the cursor
5869    /// word `b`, and split into qp/qs ignored prefix/suffix around it.
5870    /// Drives the real lexer (via `global_state_lock`'s `inittyptab`) and
5871    /// asserts the word split, cursor index, compprefix, and the qp/qs
5872    /// reconstruction — covering the marker/offset arithmetic
5873    /// (`swb-1-sqq+dq`, `p[soffs]` chuck) for the byte-consistent case.
5874    #[test]
5875    fn set_comp_sep_splits_ascii_word() {
5876        use crate::ported::zle::complete::{
5877            COMPCURRENT, COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX, COMPQUOTE, COMPWORDS,
5878        };
5879        let _g = crate::test_util::global_state_lock();
5880        let _g = zle_test_setup();
5881
5882        let setg = |g: &'static OnceLock<Mutex<String>>, v: &str| {
5883            *g.get_or_init(|| Mutex::new(String::new())).lock().unwrap() = v.to_string();
5884        };
5885        let getg = |g: &'static OnceLock<Mutex<String>>| -> String {
5886            g.get_or_init(|| Mutex::new(String::new()))
5887                .lock()
5888                .unwrap()
5889                .clone()
5890        };
5891
5892        // Reconstructed arg "a b c"; cursor between 'b' and the following
5893        // space: compprefix="a b" (active-prefix len 3), compsuffix=" c".
5894        setg(&COMPPREFIX, "a b");
5895        setg(&COMPSUFFIX, " c");
5896        setg(&COMPIPREFIX, "");
5897        setg(&COMPISUFFIX, "");
5898        setg(&COMPQIPREFIX, "");
5899        setg(&COMPQISUFFIX, "");
5900        setg(&COMPQSTACK, ""); // empty => qttype = QT_NONE (unquoted)
5901        setg(&COMPQUOTE, "");
5902        OFFS.store(0, Ordering::Relaxed);
5903        WB.store(0, Ordering::Relaxed);
5904        WE.store(0, Ordering::Relaxed);
5905        ZLEMETACS.store(0, Ordering::Relaxed);
5906        INSTRING.store(0, Ordering::Relaxed);
5907        INBACKT.store(0, Ordering::Relaxed);
5908
5909        // c:1721 — a real split happened (cur >= 0), so ret == 0.
5910        assert_eq!(set_comp_sep(), 0, "compset -q must split the ASCII word");
5911
5912        // c:1926-1934 — three words, cursor word 'b' with its injected
5913        // 'x' chucked back out.
5914        let words = COMPWORDS
5915            .get_or_init(|| Mutex::new(Vec::new()))
5916            .lock()
5917            .unwrap()
5918            .clone();
5919        assert_eq!(
5920            words,
5921            vec!["a".to_string(), "b".to_string(), "c".to_string()],
5922            "arg must re-lex into three words"
5923        );
5924        // c:1930 — 0-offset cur=1 -> 1-based compcurrent=2.
5925        assert_eq!(COMPCURRENT.load(Ordering::Relaxed), 2);
5926
5927        // c:1894-1906 — prefix/suffix of the cursor word (COMPLETEINWORD off).
5928        assert_eq!(getg(&COMPPREFIX), "b");
5929        assert_eq!(getg(&COMPSUFFIX), "");
5930
5931        // c:1912-1919 — qp/qs fold into compqiprefix/compqisuffix. The
5932        // just-prepended 1-char compqstack makes multiquote(...,1) a
5933        // no-op, so the ignored prefix/suffix are verbatim slices of the
5934        // arg; qip + word + qis must reconstruct "a b c".
5935        let recon = format!(
5936            "{}{}{}",
5937            getg(&COMPQIPREFIX),
5938            getg(&COMPPREFIX),
5939            getg(&COMPQISUFFIX)
5940        );
5941        assert_eq!(recon, "a b c", "qip + word + qis must reconstruct the arg");
5942    }
5943
5944    #[test]
5945    fn foredel_deletes_forward_from_zlemetacs() {
5946        let _g = crate::test_util::global_state_lock();
5947        let _g = zle_test_setup();
5948        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5949        // zle_utils.c:1105 — delete `ct` chars forward from ZLEMETACS.
5950        if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
5951            *g = "abcdef".to_string();
5952        }
5953        ZLEMETACS.store(2, Ordering::Relaxed);
5954        ZLEMETALL.store(6, Ordering::Relaxed);
5955        foredel(3, CUT_RAW);
5956        let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
5957        assert_eq!(line, "abf");
5958        assert_eq!(ZLEMETALL.load(Ordering::Relaxed), 3);
5959    }
5960
5961    #[test]
5962    fn inststr_inserts_at_zlemetacs() {
5963        let _g = crate::test_util::global_state_lock();
5964        let _g = zle_test_setup();
5965        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5966        // zle_tricky.c:278 — insert at cursor.
5967        if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
5968            *g = "hello".to_string();
5969        }
5970        ZLEMETACS.store(5, Ordering::Relaxed);
5971        ZLEMETALL.store(5, Ordering::Relaxed);
5972        let _ = inststr(" world");
5973        let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
5974        assert_eq!(line, "hello world");
5975        assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 11);
5976    }
5977
5978    #[test]
5979    fn metafy_and_unmetafy_roundtrip_globals() {
5980        let _g = crate::test_util::global_state_lock();
5981        let _g = zle_test_setup();
5982        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5983        // zle_tricky.c:978,995 — meta/unmeta operate on the global pair.
5984        if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
5985            *g = "plain ascii".to_string();
5986        }
5987        ZLECS.store(3, Ordering::Relaxed);
5988        ZLELL.store(11, Ordering::Relaxed);
5989        metafy_line();
5990        // For ASCII input the meta form equals the raw form.
5991        assert_eq!(
5992            ZLEMETALINE.get().unwrap().lock().unwrap().clone(),
5993            "plain ascii"
5994        );
5995        assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 3);
5996        unmetafy_line();
5997        assert_eq!(
5998            ZLELINE.get().unwrap().lock().unwrap().clone(),
5999            "plain ascii"
6000        );
6001    }
6002
6003    #[test]
6004    fn selfinsert_appends_lastchar_at_zlecs() {
6005        let _g = crate::test_util::global_state_lock();
6006        let _g = zle_test_setup();
6007        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
6008        // zle_misc.c:112-141 — insert one char at cursor, bump zlecs.
6009        //
6010        // `selfinsert()` (zle_misc.rs:180) writes through
6011        // `self_insert(c)` which mutates the `zle_main::ZLELINE`
6012        // (`Mutex<Vec<char>>`) plus `zle_main::ZLECS`/`ZLELL` —
6013        // NOT the `compcore::ZLELINE` (`OnceLock<Mutex<String>>`)
6014        // used by the meta/unmeta tests above. The original test
6015        // seeded the wrong buffer, so the assert kept seeing "ab"
6016        // (the compcore buffer never received the insert) while
6017        // self_insert silently appended 'c' to the zle_main buffer.
6018        //
6019        // Set up the zle_main buffer for the insert, then read back
6020        // from there. `zle_test_setup` already clears the zle_main
6021        // statics, so we start from a known zero state.
6022        {
6023            let mut g = crate::ported::zle::zle_main::ZLELINE.lock().unwrap();
6024            *g = "ab".chars().collect();
6025        }
6026        crate::ported::zle::zle_main::ZLECS.store(2, Ordering::Relaxed);
6027        crate::ported::zle::zle_main::ZLELL.store(2, Ordering::Relaxed);
6028        LASTCHAR.store(b'c' as i32, Ordering::Relaxed);
6029        // Force the wide-char re-derive path (lastchar_wide_valid=0 →
6030        // selfinsert refills it from LASTCHAR per zle_misc.c:119-122).
6031        LASTCHAR_WIDE_VALID.store(0, Ordering::Relaxed);
6032        let rv = selfinsert(&[]);
6033        assert_eq!(rv, 0);
6034        let buf: String = crate::ported::zle::zle_main::ZLELINE
6035            .lock()
6036            .unwrap()
6037            .iter()
6038            .collect();
6039        assert_eq!(buf, "abc");
6040        assert_eq!(
6041            crate::ported::zle::zle_main::ZLECS.load(Ordering::Relaxed),
6042            3,
6043        );
6044    }
6045
6046    #[test]
6047    fn minfo_clear_and_asked_zero_mutate_state() {
6048        let _g = crate::test_util::global_state_lock();
6049        let _g = zle_test_setup();
6050        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
6051        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
6052            let mut cm = Cmatch::default();
6053            cm.str = Some("x".into());
6054            g.cur = Some(Box::new(cm));
6055            g.asked = 1;
6056        }
6057        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
6058            g.cur = None;
6059        }
6060        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
6061            g.asked = 0;
6062        }
6063        let m = MINFO.get().unwrap().lock().unwrap().clone();
6064        assert!(m.cur.is_none());
6065        assert_eq!(m.asked, 0);
6066    }
6067
6068    #[test]
6069    fn cline_matched_stub_marks_node() {
6070        let _g = crate::test_util::global_state_lock();
6071        let _g = zle_test_setup();
6072        // compmatch.c:253 — sets CLF_MATCHED on the node chain. We
6073        // verify by running through the stub on a non-empty string
6074        // without panicking and trusting compmatch's body for the
6075        // actual flag set.
6076        cline_matched_compcore(Some("foo"));
6077        cline_matched_compcore(None);
6078        cline_matched_compcore(Some(""));
6079    }
6080
6081    #[test]
6082    fn permmatches_returns_fi_zero_when_count_present() {
6083        let _g = crate::test_util::global_state_lock();
6084        let _g = zle_test_setup();
6085        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
6086        // c:3444-3447: if ainfo->count is non-zero, fi stays 0.
6087        amatches
6088            .get_or_init(|| Mutex::new(Vec::new()))
6089            .lock()
6090            .unwrap()
6091            .clear();
6092        pmatches
6093            .get_or_init(|| Mutex::new(Vec::new()))
6094            .lock()
6095            .unwrap()
6096            .clear();
6097        if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
6098            *a = Some(Aminfo {
6099                count: 5,
6100                ..Default::default()
6101            });
6102        }
6103        newmatches.store(1, Ordering::Relaxed);
6104        let fi = permmatches(0);
6105        assert_eq!(fi, 0);
6106        assert_eq!(hasperm.load(Ordering::Relaxed), 1);
6107    }
6108
6109    /// c:1323 — `rembslash` removes backslash escapes by walking the
6110    /// string and dropping every `\` while keeping its successor.
6111    /// `\\\\` (literal `\\`) → `\` (single backslash); `\a` → `a`.
6112    /// A regression that drops the successor too would silently strip
6113    /// real chars from `path/to/\$file`.
6114    #[test]
6115    fn rembslash_unescapes_canonical_pairs() {
6116        let _g = crate::test_util::global_state_lock();
6117        assert_eq!(rembslash(r"\a"), "a");
6118        assert_eq!(rembslash(r"\\"), r"\");
6119        assert_eq!(rembslash(r"\$foo"), "$foo");
6120        assert_eq!(rembslash("plain"), "plain");
6121    }
6122
6123    /// c:1323 — empty input → empty output (the loop never runs).
6124    /// Catches a regression that returns " " or "\0" for empty input.
6125    #[test]
6126    fn rembslash_empty_input_returns_empty() {
6127        let _g = crate::test_util::global_state_lock();
6128        assert_eq!(rembslash(""), "");
6129    }
6130
6131    /// c:1323 — trailing lone `\` MUST silently drop (no following
6132    /// char to keep). C's pattern `if (let Some) { push }` is
6133    /// equivalent. Regression that pushes the literal `\` would break
6134    /// shell paths with trailing backslashes (rare but legal).
6135    #[test]
6136    fn rembslash_trailing_lone_backslash_drops_silently() {
6137        let _g = crate::test_util::global_state_lock();
6138        assert_eq!(rembslash(r"foo\"), "foo");
6139    }
6140
6141    /// c:1366 — `ctokenize` is the inverse of `untokenize`: escapes
6142    /// shell metacharacters into their tokenised forms used by the
6143    /// completion machinery. Plain alphanumerics pass through.
6144    #[test]
6145    fn ctokenize_passes_alphanumerics_through() {
6146        let _g = crate::test_util::global_state_lock();
6147        assert_eq!(ctokenize("foo123"), "foo123");
6148        assert_eq!(ctokenize(""), "");
6149        assert_eq!(ctokenize("path/to"), "path/to");
6150    }
6151
6152    /// c:1435 — `comp_quoting_string` returns one of the canonical
6153    /// quote strings: `'`, `"`, `$'`, or "" depending on `stype`.
6154    /// Catches a regression where the dispatch returns the wrong
6155    /// quote — completion would generate `cmd 'arg"` (mismatched).
6156    #[test]
6157    fn comp_quoting_string_dispatches_known_styles() {
6158        let _g = crate::test_util::global_state_lock();
6159        // The exact stype values are private to the completion impl,
6160        // but the function MUST return a non-panicking string for
6161        // every reasonable input. Probe a few values.
6162        for stype in 0..=8 {
6163            let _ = comp_quoting_string(stype);
6164        }
6165    }
6166
6167    /// c:1065 — `multiquote` with empty COMPQSTACK is a no-op (returns
6168    /// input unchanged). The stack is the per-completion quoting
6169    /// context; outside completion it's empty. Regression that quotes
6170    /// regardless would corrupt every non-completion caller.
6171    #[test]
6172    fn multiquote_empty_stack_returns_input_unchanged() {
6173        let _g = crate::test_util::global_state_lock();
6174        // Reset COMPQSTACK to empty.
6175        if let Some(c) = COMPQSTACK.get() {
6176            if let Ok(mut g) = c.lock() {
6177                g.clear();
6178            }
6179        }
6180        assert_eq!(multiquote("hello", 0), "hello");
6181        assert_eq!(multiquote("", 0), "");
6182    }
6183
6184    /// c:1092 — `tildequote("foo")` (no leading ~) MUST behave like
6185    /// multiquote — the tilde-special path is a no-op when there's no
6186    /// `~` to protect. Regression that always strips/restores would
6187    /// silently mangle non-tilde inputs.
6188    #[test]
6189    fn tildequote_non_tilde_input_unchanged() {
6190        let _g = crate::test_util::global_state_lock();
6191        // Empty COMPQSTACK + no ~ → input unchanged.
6192        if let Some(c) = COMPQSTACK.get() {
6193            if let Ok(mut g) = c.lock() {
6194                g.clear();
6195            }
6196        }
6197        assert_eq!(tildequote("foo/bar", 0), "foo/bar");
6198    }
6199
6200    /// c:1092 — empty input through tildequote is empty out.
6201    #[test]
6202    fn tildequote_empty_input_empty_output() {
6203        let _g = crate::test_util::global_state_lock();
6204        if let Some(c) = COMPQSTACK.get() {
6205            if let Ok(mut g) = c.lock() {
6206                g.clear();
6207            }
6208        }
6209        assert_eq!(tildequote("", 0), "");
6210    }
6211
6212    // ─── zsh-corpus pins for rembslash ─────────────────────────────
6213
6214    /// `rembslash("\\a\\b\\c")` strips backslashes → "abc".
6215    #[test]
6216    fn compcore_corpus_rembslash_strips_escapes() {
6217        let _g = crate::test_util::global_state_lock();
6218        assert_eq!(rembslash(r"\a\b\c"), "abc");
6219    }
6220
6221    /// `rembslash` with no backslashes is identity.
6222    #[test]
6223    fn compcore_corpus_rembslash_no_escapes_identity() {
6224        let _g = crate::test_util::global_state_lock();
6225        assert_eq!(rembslash("hello"), "hello");
6226    }
6227
6228    /// `rembslash("")` returns empty string.
6229    #[test]
6230    fn compcore_corpus_rembslash_empty_is_empty() {
6231        let _g = crate::test_util::global_state_lock();
6232        assert_eq!(rembslash(""), "");
6233    }
6234
6235    /// `rembslash("\\\\")` (escaped backslash) returns "\\".
6236    #[test]
6237    fn compcore_corpus_rembslash_escaped_backslash() {
6238        let _g = crate::test_util::global_state_lock();
6239        // r"\\" in Rust is the 2-char string "\\\\"  → one literal backslash + one literal backslash
6240        assert_eq!(rembslash(r"\\"), r"\");
6241    }
6242
6243    /// `rembslash` mid-string escape preserves surroundings.
6244    #[test]
6245    fn compcore_corpus_rembslash_preserves_context() {
6246        let _g = crate::test_util::global_state_lock();
6247        assert_eq!(rembslash(r"hello\ world"), "hello world");
6248    }
6249
6250    /// Trailing single backslash is consumed (no char after).
6251    #[test]
6252    fn compcore_corpus_rembslash_trailing_backslash_consumed() {
6253        let _g = crate::test_util::global_state_lock();
6254        assert_eq!(rembslash(r"abc\"), "abc");
6255    }
6256
6257    // ═══════════════════════════════════════════════════════════════════
6258    // Additional C-parity tests for Src/Zle/compcore.c rembslash + remsquote
6259    // + ctokenize + multiquote / tildequote string transforms.
6260    // ═══════════════════════════════════════════════════════════════════
6261
6262    /// c:1323 — `rembslash("abc")` (no backslash) is identity.
6263    #[test]
6264    fn rembslash_no_backslash_is_identity() {
6265        let _g = crate::test_util::global_state_lock();
6266        assert_eq!(rembslash("abc"), "abc");
6267        assert_eq!(rembslash("hello world"), "hello world");
6268    }
6269
6270    /// c:1323 — `rembslash` of single backslash + char drops the backslash.
6271    #[test]
6272    fn rembslash_drops_backslash_keeps_next() {
6273        let _g = crate::test_util::global_state_lock();
6274        assert_eq!(rembslash(r"\a"), "a");
6275        assert_eq!(rembslash(r"\."), ".");
6276        assert_eq!(rembslash(r"\$"), "$");
6277    }
6278
6279    /// c:1323 — repeated escapes: every `\X` collapses to `X`.
6280    #[test]
6281    fn rembslash_multiple_escapes_chain() {
6282        let _g = crate::test_util::global_state_lock();
6283        assert_eq!(rembslash(r"\a\b\c"), "abc");
6284    }
6285
6286    /// c:1343 — `remsquote("")` returns 0 (no chars consumed).
6287    #[test]
6288    fn remsquote_empty_returns_zero() {
6289        let _g = crate::test_util::global_state_lock();
6290        let mut s = String::new();
6291        let r = remsquote(&mut s);
6292        assert_eq!(r, 0);
6293        assert_eq!(s, "");
6294    }
6295
6296    /// c:1343 — `remsquote("abc")` (no quote sequences) is identity,
6297    /// returns 0.
6298    #[test]
6299    fn remsquote_no_quotes_is_identity() {
6300        let _g = crate::test_util::global_state_lock();
6301        let mut s = String::from("hello world");
6302        let r = remsquote(&mut s);
6303        assert_eq!(r, 0, "no quote sequences → 0");
6304        assert_eq!(s, "hello world", "string unchanged");
6305    }
6306
6307    /// c:1366 — `ctokenize("")` returns empty string.
6308    #[test]
6309    fn ctokenize_empty_returns_empty() {
6310        let _g = crate::test_util::global_state_lock();
6311        assert_eq!(ctokenize(""), "");
6312    }
6313
6314    /// c:1366 — `ctokenize("abc")` (no special chars) preserves content.
6315    #[test]
6316    fn ctokenize_plain_ascii_preserved() {
6317        let _g = crate::test_util::global_state_lock();
6318        // No $, {, }, or backslash → byte-for-byte preservation.
6319        let r = ctokenize("abc");
6320        assert_eq!(r.as_bytes()[0], b'a');
6321        assert_eq!(r.as_bytes()[1], b'b');
6322        assert_eq!(r.as_bytes()[2], b'c');
6323    }
6324
6325    /// c:1505 — `comp_quoting_string(0)` returns a static str (no panic).
6326    #[test]
6327    fn comp_quoting_string_returns_static_str_for_all_stypes() {
6328        let _g = crate::test_util::global_state_lock();
6329        for stype in 0..10 {
6330            let _s = comp_quoting_string(stype);
6331            // No panic = pass; returned &'static str is well-defined.
6332        }
6333    }
6334
6335    /// c:954 — `multiquote("")` returns empty.
6336    #[test]
6337    fn multiquote_empty_returns_empty() {
6338        let _g = crate::test_util::global_state_lock();
6339        let _g2 = zle_test_setup();
6340        let r = multiquote("", 0);
6341        assert_eq!(r, "");
6342    }
6343
6344    /// c:980 — `tildequote("")` returns empty.
6345    #[test]
6346    fn tildequote_empty_returns_empty() {
6347        let _g = crate::test_util::global_state_lock();
6348        let _g2 = zle_test_setup();
6349        let r = tildequote("", 0);
6350        assert_eq!(r, "");
6351    }
6352
6353    /// c:980 — `tildequote("plain")` (no tilde) is identity.
6354    #[test]
6355    fn tildequote_no_tilde_is_identity() {
6356        let _g = crate::test_util::global_state_lock();
6357        let _g2 = zle_test_setup();
6358        let r = tildequote("plain", 0);
6359        assert_eq!(r, "plain", "no tilde → input unchanged");
6360    }
6361
6362    // ═══════════════════════════════════════════════════════════════════
6363    // Additional C-parity tests for Src/Zle/compcore.c
6364    // c:936 multiquote / c:972 tildequote / c:1014 check_param /
6365    // c:1315 rembslash / c:1338 remsquote / c:1381 ctokenize /
6366    // c:1478 comp_quoting_string / c:2809 matchcmp / c:2872 matcheq
6367    // ═══════════════════════════════════════════════════════════════════
6368
6369    /// c:1315 — `rembslash("")` empty returns empty.
6370    #[test]
6371    fn rembslash_empty_returns_empty() {
6372        assert_eq!(rembslash(""), "");
6373    }
6374
6375    /// c:1315 — `rembslash` is pure.
6376    #[test]
6377    fn rembslash_is_pure() {
6378        for s in ["", "abc", r"\a", r"\\\\"] {
6379            let first = rembslash(s);
6380            for _ in 0..3 {
6381                assert_eq!(rembslash(s), first, "rembslash({:?}) must be pure", s);
6382            }
6383        }
6384    }
6385
6386    /// c:1381 — `ctokenize` is deterministic.
6387    #[test]
6388    fn ctokenize_is_deterministic() {
6389        for s in ["", "abc", "a*b", "a?b"] {
6390            let first = ctokenize(s);
6391            for _ in 0..3 {
6392                assert_eq!(
6393                    ctokenize(s),
6394                    first,
6395                    "ctokenize({:?}) must be deterministic",
6396                    s
6397                );
6398            }
6399        }
6400    }
6401
6402    /// c:1478 — `comp_quoting_string(0)` returns non-empty static.
6403    #[test]
6404    fn comp_quoting_string_zero_returns_static_str() {
6405        let _: &'static str = comp_quoting_string(0);
6406    }
6407
6408    /// c:936 — `multiquote` is pure for arbitrary input.
6409    #[test]
6410    fn multiquote_is_pure() {
6411        let _g = crate::test_util::global_state_lock();
6412        let _g2 = zle_test_setup();
6413        for s in ["", "abc", "x y"] {
6414            let first = multiquote(s, 0);
6415            for _ in 0..3 {
6416                assert_eq!(
6417                    multiquote(s, 0),
6418                    first,
6419                    "multiquote({:?}, 0) must be pure",
6420                    s
6421                );
6422            }
6423        }
6424    }
6425
6426    /// c:972 — `tildequote` is pure for non-tilde input.
6427    #[test]
6428    fn tildequote_is_pure() {
6429        let _g = crate::test_util::global_state_lock();
6430        let _g2 = zle_test_setup();
6431        for s in ["", "abc", "no_tilde", "/path/to/file"] {
6432            let first = tildequote(s, 0);
6433            for _ in 0..3 {
6434                assert_eq!(
6435                    tildequote(s, 0),
6436                    first,
6437                    "tildequote({:?}, 0) must be pure",
6438                    s
6439                );
6440            }
6441        }
6442    }
6443
6444    /// c:1338 — `remsquote(&mut empty)` returns 0.
6445    #[test]
6446    fn remsquote_empty_returns_zero_pin() {
6447        let mut s = String::new();
6448        let r = remsquote(&mut s);
6449        assert_eq!(r, 0, "empty input → 0");
6450    }
6451
6452    /// c:1014 — `check_param("")` empty returns Option<usize>.
6453    #[test]
6454    fn check_param_empty_returns_option_type() {
6455        let _g = crate::test_util::global_state_lock();
6456        let _g2 = zle_test_setup();
6457        let _: Option<usize> = check_param("", false, false);
6458    }
6459
6460    /// c:1014 — `check_param` is deterministic for empty input.
6461    #[test]
6462    fn check_param_empty_is_deterministic() {
6463        let _g = crate::test_util::global_state_lock();
6464        let _g2 = zle_test_setup();
6465        let first = check_param("", false, false);
6466        for _ in 0..3 {
6467            assert_eq!(check_param("", false, false), first);
6468        }
6469    }
6470
6471    /// c:1439 — `comp_str(false)` returns (String, i32, i32) tuple.
6472    #[test]
6473    fn comp_str_returns_string_i32_i32_tuple() {
6474        let _g = crate::test_util::global_state_lock();
6475        let _g2 = zle_test_setup();
6476        let _: (String, i32, i32) = comp_str(false);
6477    }
6478
6479    // ═══════════════════════════════════════════════════════════════════
6480    // Additional C-parity tests for Src/Zle/compcore.c
6481    // c:1505 set_comp_sep / c:1544 set_list_array / c:1555 get_user_var /
6482    // c:1645 get_data_arr / c:2681 begcmgroup / c:2735 endcmgroup /
6483    // c:2749 addexpl / c:1478 comp_quoting_string
6484    // ═══════════════════════════════════════════════════════════════════
6485
6486    /// c:1505 — `set_comp_sep` returns i32 (compile-time type pin).
6487    #[test]
6488    fn set_comp_sep_returns_i32_type() {
6489        let _g = crate::test_util::global_state_lock();
6490        let _g2 = zle_test_setup();
6491        let _: i32 = set_comp_sep();
6492    }
6493
6494    /// c:1555 — `get_user_var(None)` returns Option<Vec<String>>.
6495    #[test]
6496    fn get_user_var_returns_option_vec_string_type() {
6497        let _g = crate::test_util::global_state_lock();
6498        let _g2 = zle_test_setup();
6499        let _: Option<Vec<String>> = get_user_var(None);
6500    }
6501
6502    /// c:1555 — `get_user_var(None)` is deterministic.
6503    #[test]
6504    fn get_user_var_none_is_deterministic() {
6505        let _g = crate::test_util::global_state_lock();
6506        let _g2 = zle_test_setup();
6507        let first = get_user_var(None);
6508        for _ in 0..3 {
6509            assert_eq!(
6510                get_user_var(None),
6511                first,
6512                "get_user_var(None) must be deterministic"
6513            );
6514        }
6515    }
6516
6517    /// c:1645 — `get_data_arr("", false)` returns Option<Vec<String>>.
6518    #[test]
6519    fn get_data_arr_returns_option_vec_string_type() {
6520        let _g = crate::test_util::global_state_lock();
6521        let _g2 = zle_test_setup();
6522        let _: Option<Vec<String>> = get_data_arr("", false);
6523    }
6524
6525    /// c:1645 — `get_data_arr("", _)` empty name returns None.
6526    #[test]
6527    fn get_data_arr_empty_name_returns_none() {
6528        let _g = crate::test_util::global_state_lock();
6529        let _g2 = zle_test_setup();
6530        assert!(get_data_arr("", false).is_none(), "empty name → None");
6531        assert!(
6532            get_data_arr("", true).is_none(),
6533            "empty name (keys=true) → None"
6534        );
6535    }
6536
6537    /// c:2681 — `begcmgroup(None, 0)` safe.
6538    #[test]
6539    fn begcmgroup_none_no_panic() {
6540        let _g = crate::test_util::global_state_lock();
6541        let _g2 = zle_test_setup();
6542        begcmgroup(None, 0);
6543    }
6544
6545    /// c:2735 — `endcmgroup(None)` safe.
6546    #[test]
6547    fn endcmgroup_none_no_panic() {
6548        let _g = crate::test_util::global_state_lock();
6549        let _g2 = zle_test_setup();
6550        endcmgroup(None);
6551    }
6552
6553    /// c:2681 + c:2735 — beg/end cmgroup round-trip safe.
6554    #[test]
6555    fn begcmgroup_endcmgroup_round_trip_safe() {
6556        let _g = crate::test_util::global_state_lock();
6557        let _g2 = zle_test_setup();
6558        for _ in 0..3 {
6559            begcmgroup(None, 0);
6560            endcmgroup(None);
6561        }
6562    }
6563
6564    /// c:2749 — `addexpl(false)` safe.
6565    #[test]
6566    fn addexpl_false_no_panic() {
6567        let _g = crate::test_util::global_state_lock();
6568        let _g2 = zle_test_setup();
6569        addexpl(false);
6570    }
6571
6572    /// c:1544 — `set_list_array("", &[])` empty inputs is safe.
6573    #[test]
6574    fn set_list_array_empty_no_panic() {
6575        let _g = crate::test_util::global_state_lock();
6576        let _g2 = zle_test_setup();
6577        set_list_array("", &[]);
6578    }
6579
6580    /// c:1478 — `comp_quoting_string(N)` returns &'static str (type pin).
6581    #[test]
6582    fn comp_quoting_string_returns_static_str_type() {
6583        let _: &'static str = comp_quoting_string(0);
6584    }
6585
6586    /// c:1478 — `comp_quoting_string` is pure across stypes.
6587    #[test]
6588    fn comp_quoting_string_pure_across_stypes() {
6589        for stype in 0..10 {
6590            let first = comp_quoting_string(stype);
6591            for _ in 0..3 {
6592                assert_eq!(
6593                    comp_quoting_string(stype),
6594                    first,
6595                    "comp_quoting_string({}) must be pure",
6596                    stype
6597                );
6598            }
6599        }
6600    }
6601}