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 std::sync::atomic::{AtomicI32, Ordering};
25use std::sync::{Mutex, OnceLock};
26
27use crate::ported::zsh_h::{
28
29
30    Bnull, Inbrace, Outbrace, QT_BACKSLASH, QT_DOLLARS, QT_DOUBLE, QT_SINGLE, Stringg,
31};
32
33// --- AUTO: cross-zle hoisted-fn use glob ---
34#[allow(unused_imports)]
35#[allow(unused_imports)]
36use crate::ported::zle::zle_main::*;
37#[allow(unused_imports)]
38use crate::ported::zle::zle_misc::*;
39#[allow(unused_imports)]
40use crate::ported::zle::zle_hist::*;
41#[allow(unused_imports)]
42use crate::ported::zle::zle_move::*;
43#[allow(unused_imports)]
44use crate::ported::zle::zle_word::*;
45#[allow(unused_imports)]
46use crate::ported::zle::zle_params::*;
47#[allow(unused_imports)]
48use crate::ported::zle::zle_vi::*;
49#[allow(unused_imports)]
50use crate::ported::zle::zle_utils::*;
51#[allow(unused_imports)]
52use crate::ported::zle::zle_refresh::*;
53#[allow(unused_imports)]
54use crate::ported::zle::zle_tricky::*;
55#[allow(unused_imports)]
56use crate::ported::zle::textobjects::*;
57#[allow(unused_imports)]
58use crate::ported::zle::deltochar::*;
59use crate::ported::zle::comp_h::{
60    Aminfo, Cexpl, Cmatch, Cmgroup, CGF_MATSORT, CGF_NOSORT, CGF_NUMSORT, CGF_REVSORT,
61    CGF_UNIQALL, CGF_UNIQCON, CMF_DELETE, CMF_DISPLINE, CMF_FMULT, CMF_MULT, CMF_NOLIST,
62    CMF_PACKED, CMF_PARBR, CMF_PARNEST, CMF_ROWS,
63};
64use crate::ported::zle::complete::{COMPIPREFIX, COMPPREFIX, COMPSUFFIX};
65use crate::ported::zle::zle_tricky::{MENUCMP, USEMENU};
66use crate::ported::zle::complete::COMPLIST;
67use crate::ported::zle::zle_tricky::{USEGLOB, WOULDINSTAB};
68use crate::ported::zsh_h::{Dnull, Equals, Hat, Inbrack, Inpar, Outpar, Pound, Qstring, Quest, Snull, Star, Tilde};
69use crate::ported::zle::comp_h::{CAF_ALL, CAF_MATSORT, CAF_NOSORT, CAF_NUMSORT, CAF_QUOTE, CAF_REVSORT, CAF_UNIQALL, CAF_UNIQCON};
70
71// =====================================================================
72// Extern globals — declared in other C files, mirrored here per
73// PORT.md Rule 9 ("stub the EXTERN dependencies ... locally with
74// file:line citations to their home file") so the local body ports
75// below have a value source. When the canonical Rust homes land,
76// these become `pub use crate::ported::<canonical>::*` re-exports.
77// =====================================================================
78
79/// Port of `mod_export int wb` from `Src/lex.c:120`. Word-begin
80/// position in the metafied line for the currently-completing word.
81pub static WB: AtomicI32 = AtomicI32::new(0);                                // lex.c:120
82/// Port of `mod_export int we` from `Src/lex.c:120`. Word-end position.
83pub static WE: AtomicI32 = AtomicI32::new(0);                                // lex.c:120
84/// Port of `mod_export int zlemetacs` from `Src/lex.c:104`. Cursor
85/// position in the metafied line.
86pub static ZLEMETACS: AtomicI32 = AtomicI32::new(0);                         // lex.c:104
87/// Port of `mod_export int zlemetall` from `Src/lex.c:104`. Length
88/// of the metafied line.
89pub static ZLEMETALL: AtomicI32 = AtomicI32::new(0);                         // lex.c:104
90/// Port of `mod_export int addedx` from `Src/lex.c:115`. Non-zero
91/// while a dummy `x` cursor marker is in the line being lexed
92/// (so completion can capture the partial word at the cursor).
93pub static ADDEDX: AtomicI32 = AtomicI32::new(0);                            // lex.c:115
94
95/// Port of `mod_export char *zlemetaline` from `Src/lex.c:103`. The
96/// metafied edit buffer for the current ZLE session — `foredel`,
97/// `inststr`, `selfinsert` operate on this directly when compcore's
98/// error-recovery path fires (compcore.c:344-355).
99pub static ZLEMETALINE: OnceLock<Mutex<String>> = OnceLock::new();           // lex.c:103
100/// Port of `mod_export ZLE_STRING_T zleline` from `Src/zle_main.c`.
101pub static ZLELINE: OnceLock<Mutex<String>> = OnceLock::new();               // zle_main.c
102/// Port of `mod_export int zlecs` from `Src/zle_main.c`.
103pub static ZLECS: AtomicI32 = AtomicI32::new(0);                             // zle_main.c
104/// Port of `mod_export int zlell` from `Src/zle_main.c`.
105pub static ZLELL: AtomicI32 = AtomicI32::new(0);                             // zle_main.c
106/// Port of `mod_export int inwhat` from `Src/lex.c:110`. Lex context
107/// classification — IN_NOTHING / IN_CMD / IN_COND / IN_MATH / IN_PAR /
108/// IN_ENV.
109pub static INWHAT: AtomicI32 = AtomicI32::new(0);                            // lex.c:110
110/// Port of `mod_export int zmult` from `Src/zle_main.c`. Numeric
111/// prefix multiplier for the current ZLE command.
112pub static ZMULT: AtomicI32 = AtomicI32::new(1);                             // zle_main.c
113/// Port of `mod_export char *compfunc` from `Src/Zle/zle_tricky.c:143`.
114/// Name of the user completion shell function — non-empty when the
115/// new completion system (`compsys`) is active; empty for compctl.
116pub static compfunc: OnceLock<Mutex<Option<String>>> = OnceLock::new();      // zle_tricky.c:143
117/// Port of `mod_export char *comppatmatch` from `Src/Zle/zle_tricky.c`.
118/// `$compstate[pattern_match]` — when non-empty + non-"\0" enables
119/// pattern-aware matching for parameter-name completion.
120pub static comppatmatch: OnceLock<Mutex<Option<String>>> = OnceLock::new();
121/// Port of `mod_export char *compqstack` from `Src/Zle/compcore.c`.
122/// Quoting-state stack (1 char per nesting level).
123pub static compqstack: OnceLock<Mutex<String>> = OnceLock::new();
124
125// Brace counters live in zle_tricky.c:114 — re-exported there. Local
126// re-exports here so call sites stay short:
127#[doc(hidden)]
128pub use crate::ported::zle::zle_tricky::{NBRBEG as _NBRBEG, NBREND as _NBREND};
129use crate::zsh_h::{isset, BASHAUTOLIST, NUMERICGLOBSORT, RCQUOTES, SORTIT_IGNORING_BACKSLASHES, SORTIT_NUMERICALLY};
130// =====================================================================
131// File-scope globals — `Src/Zle/compcore.c:36-279`.
132// =====================================================================
133
134/// Port of `int useexact` from compcore.c:36.
135pub static useexact: AtomicI32 = AtomicI32::new(0);                          // c:36
136/// Port of `int useline` from compcore.c:36.
137pub static useline: AtomicI32 = AtomicI32::new(0);                           // c:36
138/// Port of `int uselist` from compcore.c:36.
139pub static uselist: AtomicI32 = AtomicI32::new(0);                           // c:36
140/// Port of `int forcelist` from compcore.c:36.
141pub static forcelist: AtomicI32 = AtomicI32::new(0);                         // c:36
142/// Port of `int startauto` from compcore.c:36.
143pub static startauto: AtomicI32 = AtomicI32::new(0);                         // c:36
144
145/// Port of `mod_export int iforcemenu` from compcore.c:39.
146pub static iforcemenu: AtomicI32 = AtomicI32::new(0);                        // c:39
147
148/// Port of `mod_export int dolastprompt` from compcore.c:44.
149pub static dolastprompt: AtomicI32 = AtomicI32::new(0);                      // c:44
150
151/// Port of `mod_export int oldlist` from compcore.c:49.
152pub static oldlist: AtomicI32 = AtomicI32::new(0);                           // c:49
153/// Port of `mod_export int oldins` from compcore.c:49.
154pub static oldins: AtomicI32 = AtomicI32::new(0);                            // c:49
155
156/// Port of `int origlpre` from compcore.c:54.
157pub static origlpre: AtomicI32 = AtomicI32::new(0);                          // c:54
158/// Port of `int origlsuf` from compcore.c:54.
159pub static origlsuf: AtomicI32 = AtomicI32::new(0);                          // c:54
160/// Port of `int lenchanged` from compcore.c:54.
161pub static lenchanged: AtomicI32 = AtomicI32::new(0);                        // c:54
162
163/// Port of `int movetoend` from compcore.c:61.
164pub static movetoend: AtomicI32 = AtomicI32::new(0);                         // c:61
165
166/// Port of `mod_export int insmnum` from compcore.c:66.
167pub static insmnum: AtomicI32 = AtomicI32::new(0);                           // c:66
168/// Port of `mod_export int insspace` from compcore.c:66.
169pub static insspace: AtomicI32 = AtomicI32::new(0);                          // c:66
170
171/// Port of `mod_export int menuacc` from compcore.c:81.
172pub static menuacc: AtomicI32 = AtomicI32::new(0);                           // c:81
173
174/// Port of `int hasunqu` from compcore.c:86.
175pub static hasunqu: AtomicI32 = AtomicI32::new(0);                           // c:86
176/// Port of `int useqbr` from compcore.c:86.
177pub static useqbr: AtomicI32 = AtomicI32::new(0);                            // c:86
178/// Port of `int brpcs` from compcore.c:86.
179pub static brpcs: AtomicI32 = AtomicI32::new(0);                             // c:86
180/// Port of `int brscs` from compcore.c:86.
181pub static brscs: AtomicI32 = AtomicI32::new(0);                             // c:86
182
183/// Port of `mod_export int ispar` from compcore.c:91.
184pub static ispar: AtomicI32 = AtomicI32::new(0);                             // c:91
185/// Port of `mod_export int linwhat` from compcore.c:91.
186pub static linwhat: AtomicI32 = AtomicI32::new(0);                           // c:91
187
188/// Port of `char *parpre` from compcore.c:96.
189pub static parpre: OnceLock<Mutex<String>> = OnceLock::new();                // c:96
190
191/// Port of `int parflags` from compcore.c:101.
192pub static parflags: AtomicI32 = AtomicI32::new(0);                          // c:101
193
194/// Port of `mod_export int mflags` from compcore.c:106.
195pub static mflags: AtomicI32 = AtomicI32::new(0);                            // c:106
196
197/// Port of `int parq` from compcore.c:111.
198pub static parq: AtomicI32 = AtomicI32::new(0);                              // c:111
199/// Port of `int eparq` from compcore.c:111.
200pub static eparq: AtomicI32 = AtomicI32::new(0);                             // c:111
201
202/// Port of `mod_export char *ipre` from compcore.c:118.
203pub static ipre: OnceLock<Mutex<String>> = OnceLock::new();                  // c:118
204/// Port of `mod_export char *ripre` from compcore.c:118.
205pub static ripre: OnceLock<Mutex<String>> = OnceLock::new();                 // c:118
206/// Port of `mod_export char *isuf` from compcore.c:118.
207pub static isuf: OnceLock<Mutex<String>> = OnceLock::new();                  // c:118
208
209/// Port of `mod_export LinkList matches` from compcore.c:124.
210pub static matches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new();          // c:124
211/// Port of `LinkList fmatches` from compcore.c:126.
212pub static fmatches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new();         // c:126
213
214/// Port of `mod_export Cmgroup amatches` from compcore.c:135.
215pub static amatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new();        // c:135
216/// Port of `mod_export Cmgroup pmatches` from compcore.c:135.
217pub static pmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new();        // c:135
218/// Port of `mod_export Cmgroup lastmatches` from compcore.c:135.
219pub static lastmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new();     // c:135
220/// Port of `mod_export Cmgroup lmatches` from compcore.c:135. Last
221/// element pointer in the perm list; here a single-slot holder.
222pub static lmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new();     // c:135
223/// Port of `mod_export Cmgroup lastlmatches` from compcore.c:135.
224pub static lastlmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
225
226/// Port of `mod_export int hasoldlist` from compcore.c:140.
227pub static hasoldlist: AtomicI32 = AtomicI32::new(0);                        // c:140
228/// Port of `mod_export int hasperm` from compcore.c:140.
229pub static hasperm: AtomicI32 = AtomicI32::new(0);                           // c:140
230/// Port of `int hasallmatch` from compcore.c:145.
231pub static hasallmatch: AtomicI32 = AtomicI32::new(0);                       // c:145
232
233/// Port of `mod_export int newmatches` from compcore.c:150.
234pub static newmatches: AtomicI32 = AtomicI32::new(0);                        // c:150
235
236/// Port of `mod_export int permmnum` from compcore.c:155.
237pub static permmnum: AtomicI32 = AtomicI32::new(0);                          // c:155
238/// Port of `mod_export int permgnum` from compcore.c:155.
239pub static permgnum: AtomicI32 = AtomicI32::new(0);                          // c:155
240/// Port of `mod_export int lastpermmnum` from compcore.c:155.
241pub static lastpermmnum: AtomicI32 = AtomicI32::new(0);                      // c:155
242/// Port of `mod_export int lastpermgnum` from compcore.c:155.
243pub static lastpermgnum: AtomicI32 = AtomicI32::new(0);                      // c:155
244
245/// Port of `mod_export int nmatches` from compcore.c:160.
246pub static nmatches: AtomicI32 = AtomicI32::new(0);                          // c:160
247/// Port of `mod_export int smatches` from compcore.c:162.
248pub static smatches: AtomicI32 = AtomicI32::new(0);                          // c:162
249
250/// Port of `mod_export int diffmatches` from compcore.c:167.
251pub static diffmatches: AtomicI32 = AtomicI32::new(0);                       // c:167
252
253/// Port of `mod_export int nmessages` from compcore.c:172.
254pub static nmessages: AtomicI32 = AtomicI32::new(0);                         // c:172
255
256/// Port of `mod_export int onlyexpl` from compcore.c:177.
257pub static onlyexpl: AtomicI32 = AtomicI32::new(0);                          // c:177
258
259/// Port of `mod_export struct cldata listdat` from compcore.c:182.
260pub static listdat: OnceLock<Mutex<crate::ported::zle::comp_h::Cldata>> =
261    OnceLock::new();                                                         // c:182
262
263/// Port of `mod_export int ispattern` from compcore.c:187.
264pub static ispattern: AtomicI32 = AtomicI32::new(0);                         // c:187
265/// Port of `mod_export int haspattern` from compcore.c:187.
266pub static haspattern: AtomicI32 = AtomicI32::new(0);                        // c:187
267
268/// Port of `mod_export int hasmatched` from compcore.c:192.
269pub static hasmatched: AtomicI32 = AtomicI32::new(0);                        // c:192
270/// Port of `mod_export int hasunmatched` from compcore.c:192.
271pub static hasunmatched: AtomicI32 = AtomicI32::new(0);                      // c:192
272
273/// Port of `Cmgroup mgroup` from compcore.c:197.
274pub static mgroup: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new();       // c:197
275
276/// Port of `mod_export int mnum` from compcore.c:202.
277pub static mnum: AtomicI32 = AtomicI32::new(0);                              // c:202
278
279/// Port of `mod_export int unambig_mnum` from compcore.c:207.
280pub static unambig_mnum: AtomicI32 = AtomicI32::new(0);                      // c:207
281
282/// Port of `int maxmlen` from compcore.c:212.
283pub static maxmlen: AtomicI32 = AtomicI32::new(0);                           // c:212
284/// Port of `int minmlen` from compcore.c:212.
285pub static minmlen: AtomicI32 = AtomicI32::new(0);                           // c:212
286
287/// Port of `LinkList expls` from compcore.c:218.
288pub static expls: OnceLock<Mutex<Vec<Cexpl>>> = OnceLock::new();             // c:218
289
290/// Port of `mod_export Cexpl curexpl` from compcore.c:221.
291pub static curexpl: OnceLock<Mutex<Option<Cexpl>>> = OnceLock::new();        // c:221
292
293/// Port of `LinkList matchers` from compcore.c:236.
294pub static matchers: OnceLock<Mutex<Vec<String>>> = OnceLock::new();         // c:236
295
296/// Port of `mod_export Aminfo ainfo` from compcore.c:246.
297pub static ainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new();         // c:246
298/// Port of `mod_export Aminfo fainfo` from compcore.c:246.
299pub static fainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new();        // c:246
300
301/// Port of `mod_export LinkList allccs` from compcore.c:259.
302pub static allccs: OnceLock<Mutex<Vec<String>>> = OnceLock::new();           // c:259
303
304/// Port of `int fromcomp` from compcore.c:271.
305pub static fromcomp: AtomicI32 = AtomicI32::new(0);                          // c:271
306
307/// Port of `mod_export int lastend` from compcore.c:276.
308pub static lastend: AtomicI32 = AtomicI32::new(0);                           // c:276
309
310/// Port of `static int oldmenucmp` from compcore.c:457.
311pub static OLDMENUCMP: AtomicI32 = AtomicI32::new(0);                        // c:457
312
313/// Port of `static int parwb` from compcore.c:540.
314pub static PARWB: AtomicI32 = AtomicI32::new(0);                             // c:540
315/// Port of `static int parwe` from compcore.c:540.
316pub static PARWE: AtomicI32 = AtomicI32::new(0);                             // c:540
317/// Port of `static int paroffs` from compcore.c:540.
318pub static PAROFFS: AtomicI32 = AtomicI32::new(0);                           // c:540
319
320/// Port of `static int matchorder` from compcore.c:3169.
321pub static MATCHORDER: AtomicI32 = AtomicI32::new(0);                        // c:3169
322
323// =====================================================================
324// rembslash — `Src/Zle/compcore.c:1323`.
325// =====================================================================
326
327/// Port of `mod_export char *rembslash(char *s)` from compcore.c:1322.
328///
329/// "Strip backslash escapes from a token, treating `\X` as `X`."
330pub fn rembslash(s: &str) -> String {                                        // c:1323
331    let mut result = String::with_capacity(s.len());                         // c:1323
332    let mut chars = s.chars().peekable();                                    // c:1327
333    while let Some(c) = chars.next() {
334        if c == '\\' {                                                       // c:1328
335            if let Some(nxt) = chars.next() {                                // c:1329
336                result.push(nxt);
337            }
338        } else {
339            result.push(c);                                                  // c:1343-1333
340        }
341    }
342    result                                                                   // c:1343
343}
344
345// =====================================================================
346// remsquote — `Src/Zle/compcore.c:1343`.
347// =====================================================================
348
349/// Port of `mod_export int remsquote(char *s)` from compcore.c:1342.
350pub fn remsquote(s: &mut String) -> i32 {                                    // c:1343
351    let rcquotes = isset(RCQUOTES); // c:1343
352    let qa: usize = if rcquotes { 1 } else { 3 };
353
354    let bytes = s.as_bytes();                                                // c:1346
355    let mut t = Vec::<u8>::with_capacity(bytes.len());
356    let mut ret: i32 = 0;
357    let mut i = 0;
358    while i < bytes.len() {                                                  // c:1348
359        let matched = if qa == 1 {                                           // c:1349
360            i + 1 < bytes.len() && bytes[i] == b'\'' && bytes[i + 1] == b'\''
361        } else {
362            i + 3 < bytes.len()                                              // c:1351
363                && bytes[i]     == b'\''
364                && bytes[i + 1] == b'\\'
365                && bytes[i + 2] == b'\''
366                && bytes[i + 3] == b'\''
367        };
368        if matched {
369            ret += qa as i32;                                                // c:1352
370            t.push(b'\'');                                                   // c:1353
371            i += qa + 1;                                                     // c:1354
372        } else {
373            t.push(bytes[i]);                                                // c:1356
374            i += 1;
375        }
376    }
377    *s = String::from_utf8(t).unwrap_or_default();                           // c:1357
378    ret                                                                      // c:1366
379}
380
381// =====================================================================
382// ctokenize — `Src/Zle/compcore.c:1366`.
383// =====================================================================
384
385/// Port of `mod_export char *ctokenize(char *p)` from compcore.c:1365.
386///
387/// C calls `tokenize(p)` first then walks the string replacing
388/// unescaped `$`/`{`/`}` with the token bytes `String`/`Inbrace`/
389/// `Outbrace`. Backslash-escaped variants become `Bnull`.
390pub fn ctokenize(p: &str) -> String {                                        // c:1366
391    let bytes = p.as_bytes();                                                // c:1366
392    let mut out = Vec::<u8>::with_capacity(bytes.len());
393    let mut bslash = false;                                                  // c:1369
394    let mut prev_idx: Option<usize> = None;
395    let mut i = 0;
396    while i < bytes.len() {
397        let b = bytes[i];                                                    // c:1373
398        if b == b'\\' {                                                      // c:1374
399            bslash = true;
400            out.push(b);
401            prev_idx = Some(out.len() - 1);
402        } else {
403            if b == b'$' || b == b'{' || b == b'}' {                         // c:1377
404                if bslash {                                                  // c:1378
405                    if let Some(pi) = prev_idx {                             // c:1379
406                        out.truncate(pi);
407                        let mut buf = [0u8; 4];
408                        out.extend_from_slice(Bnull.encode_utf8(&mut buf).as_bytes());
409                    }
410                    out.push(b);
411                } else {
412                    let tok = if b == b'$' { Stringg }                    // c:1381
413                              else if b == b'{' { Inbrace }                  // c:1382
414                              else { Outbrace };                             // c:1382
415                    let mut buf = [0u8; 4];
416                    out.extend_from_slice(tok.encode_utf8(&mut buf).as_bytes());
417                }
418            } else {
419                out.push(b);
420            }
421            bslash = false;                                                  // c:1384
422            prev_idx = Some(out.len().saturating_sub(1));
423        }
424        i += 1;
425    }
426    String::from_utf8(out).unwrap_or_default()                               // c:1403
427}
428
429// =====================================================================
430// comp_str — `Src/Zle/compcore.c:1403`.
431// =====================================================================
432
433/// Port of `mod_export char *comp_str(int *ipl, int *pl, int untok)`
434/// from compcore.c:1402.
435pub fn comp_str(untok: bool) -> (String, i32, i32) {                         // c:1403
436    let mut p = COMPPREFIX.get_or_init(|| Mutex::new(String::new()))         // c:1405
437        .lock().unwrap().clone();
438    let mut s = COMPSUFFIX.get_or_init(|| Mutex::new(String::new()))         // c:1406
439        .lock().unwrap().clone();
440    let ip = COMPIPREFIX.get_or_init(|| Mutex::new(String::new()))           // c:1407
441        .lock().unwrap().clone();
442    if !untok {                                                              // c:1411
443        p = ctokenize(&p);                                                   // c:1412
444        p = p.chars().filter(|&c| c != Bnull).collect();                     // c:1413 remnulargs
445        s = ctokenize(&s);                                                   // c:1414
446        s = s.chars().filter(|&c| c != Bnull).collect();                     // c:1415
447    }
448    let lp = p.len() as i32;                                                 // c:1417
449    let lip = ip.len() as i32;                                               // c:1419
450    let mut str = String::with_capacity(ip.len() + p.len() + s.len() + 1);  // c:1420
451    str.push_str(&ip);                                                      // c:1435
452    str.push_str(&p);                                                       // c:1435
453    str.push_str(&s);                                                       // c:1435
454    (str, lip, lp)                                                          // c:1435-1430
455}
456
457// =====================================================================
458// comp_quoting_string — `Src/Zle/compcore.c:1435`.
459// =====================================================================
460
461/// Port of `mod_export char *comp_quoting_string(int stype)` from
462/// compcore.c:1434.
463pub fn comp_quoting_string(stype: i32) -> &'static str {                     // c:1435
464    match stype {                                                            // c:1435
465        x if x == QT_SINGLE  => "'",                                         // c:1439-1440
466        x if x == QT_DOUBLE  => "\"",                                        // c:1441-1442
467        x if x == QT_DOLLARS => "$'",                                        // c:1443-1444
468        _ => {                                                               // c:1445
469            let _ = QT_BACKSLASH;
470            "\\"                                                             // c:1446
471        }
472    }
473}
474
475// =====================================================================
476// multiquote — `Src/Zle/compcore.c:1065`.
477// =====================================================================
478
479/// Port of `mod_export char *multiquote(char *s, int ign)` from
480/// compcore.c:1064.
481pub fn multiquote(s: &str, ign: i32) -> String {                             // c:1065
482    let stack = crate::ported::zle::complete::COMPQSTACK                     // c:1065
483        .get_or_init(|| Mutex::new(String::new()))
484        .lock()
485        .map(|g| g.clone())
486        .unwrap_or_default();
487    let p_bytes = stack.as_bytes();
488    if !p_bytes.is_empty() && (ign == 0 || p_bytes.len() > 1) {              // c:1070
489        let start = if ign != 0 { 1 } else { 0 };                            // c:1071
490        let mut cur = s.to_string();
491        for &q in &p_bytes[start..] {                                        // c:1073
492            let qt = match q as i32 {                                        // c:1074
493                x if x == QT_BACKSLASH => crate::ported::zsh_h::QT_BACKSLASH,
494                x if x == QT_SINGLE    => crate::ported::zsh_h::QT_SINGLE,
495                x if x == QT_DOUBLE    => crate::ported::zsh_h::QT_DOUBLE,
496                x if x == QT_DOLLARS   => crate::ported::zsh_h::QT_DOLLARS,
497                _ => crate::ported::zsh_h::QT_BACKSLASH,
498            };
499            cur = crate::ported::utils::quotestring(&cur, qt);
500        }
501        cur                                                                  // c:1092
502    } else {
503        s.to_string()                                                        // c:1092
504    }
505}
506
507// =====================================================================
508// tildequote — `Src/Zle/compcore.c:1092`.
509// =====================================================================
510
511/// Port of `mod_export char *tildequote(char *s, int ign)` from
512/// compcore.c:1091.
513pub fn tildequote(s: &str, ign: i32) -> String {                             // c:1092
514    let bytes = s.as_bytes();                                                // c:1092
515    let tilde = !bytes.is_empty() && bytes[0] == b'~';                       // c:1097
516    let staged = if tilde {                                                  // c:1098
517        let mut tmp = String::with_capacity(s.len());
518        tmp.push('x');
519        tmp.push_str(&s[1..]);
520        tmp
521    } else {
522        s.to_string()
523    };
524    let mut quoted = multiquote(&staged, ign);                               // c:1099
525    if tilde && !quoted.is_empty() {                                         // c:1100
526        let mut new_q = String::with_capacity(quoted.len());
527        let mut swapped = false;
528        for c in quoted.chars() {
529            if !swapped && c == 'x' {
530                new_q.push('~');
531                swapped = true;
532            } else {
533                new_q.push(c);
534            }
535        }
536        quoted = new_q;
537    }
538    quoted                                                                   // c:1101
539}
540
541// =====================================================================
542// before_complete / after_complete — `Src/Zle/compcore.c:461 / 503`.
543// =====================================================================
544
545/// Direct port of `int before_complete(Hookdef dummy, int *lst)`
546/// from `Src/Zle/compcore.c:461`. Pre-completion hook: snapshots
547/// `menucmp` into `oldmenucmp`, decides whether the current state
548/// shortcircuits via menu-completion, clamps the cursor when re-
549/// entering an in-word completion, and toggles automenu mode.
550/// Returns 1 to suppress the next-stage match build, 0 to continue.
551pub fn before_complete(lst: &mut i32) -> i32 {                               // c:461
552    use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
553    use crate::ported::zle::zle_tricky::{LASTAMBIG, SHOWAGAIN, VALIDLIST};
554    use crate::ported::zle::zle_refresh::SHOWINGLIST;
555
556    // c:463 — `oldmenucmp = menucmp;`
557    OLDMENUCMP.store(MENUCMP.load(Ordering::Relaxed), Ordering::Relaxed);
558
559    // c:465-466 — `if (showagain && validlist) showinglist = -2;`
560    if SHOWAGAIN.load(Ordering::Relaxed) != 0
561        && VALIDLIST.load(Ordering::Relaxed) != 0
562    {
563        SHOWINGLIST.store(-2, Ordering::Relaxed);
564    }
565    // c:467 — `showagain = 0;`
566    SHOWAGAIN.store(0, Ordering::Relaxed);
567
568    let has_cur = MINFO.get().and_then(|m| m.lock().ok())
569        .map(|m| m.cur.is_some())
570        .unwrap_or(false);
571    let menucmp_v = MENUCMP.load(Ordering::Relaxed);
572
573    // c:471-474 — menu-completion shortcircuit (non-listing path).
574    if has_cur && menucmp_v != 0 && *lst != COMP_LIST_EXPAND {
575        // C: `do_menucmp(*lst); return 1;` — Rust signature takes a
576        // match list; the side-effect of advancing minfo lives in
577        // do_menucmp. The post-summary `do_menucmp` Rust port has a
578        // (&[String], cur, fwd) → (idx, &str) shape, which doesn't
579        // fit a void-context call. The salient signal here is the
580        // `return 1` short-circuit; preserve that.
581        return 1;                                                            // c:473
582    }
583    // c:475-479 — menu-completion shortcircuit (listing path).
584    if has_cur && menucmp_v != 0
585        && VALIDLIST.load(Ordering::Relaxed) != 0
586        && *lst == COMP_LIST_COMPLETE
587    {
588        SHOWINGLIST.store(-2, Ordering::Relaxed);
589        onlyexpl.store(0, Ordering::Relaxed);                                // c:477
590        // c:477 — `listdat.valid = 0;`
591        if let Some(ld) = listdat.get() {
592            if let Ok(mut g) = ld.lock() {
593                g.valid = 0;
594            }
595        }
596        return 1;                                                            // c:478
597    }
598
599    // c:489-490 — `if ((fromcomp & FC_INWORD) && (zlecs = lastend) > zlell)
600    //              zlecs = zlell;`
601    //              fromcomp/lastend globals not yet ported. Substrate
602    //              gap documented; skip this branch until they arrive.
603    //              Cursor clamp matters only when re-entering an
604    //              in-word completion, so skipping is observable only
605    //              during interactive composition where the
606    //              completion engine itself isn't wired yet.
607
608    // c:494-496 — automenu trigger.
609    if startauto.load(Ordering::Relaxed) != 0
610        && LASTAMBIG.load(Ordering::Relaxed) != 0
611    {
612        let bashauto = isset(BASHAUTOLIST);
613        let last = LASTAMBIG.load(Ordering::Relaxed);
614        if !bashauto || last == 2 {
615            USEMENU.store(2, Ordering::Relaxed);
616        }
617    }
618
619    0                                                                        // c:498
620}
621
622/// Direct port of `int after_complete(Hookdef dummy, int *dat)`
623/// from `Src/Zle/compcore.c:503`. Post-completion hook: when a
624/// completion has just transitioned into menu-completion (menucmp
625/// went 0→1 across this round), runs MENUSTARTHOOK so registered
626/// hook fns can veto or modify the about-to-display menu.
627///
628/// Hook handlers are registered via `addhookfunc("menu_start", fn)`
629/// (see `crate::ported::module::addhookfunc`), which writes to the
630/// global HOOKTAB. C's `comphooks[]` table declares `menu_start` as
631/// HOOKF_ALL, so every handler fires and the first non-zero return
632/// short-circuits the chain (see runhookdef at module.c:990).
633///
634/// Return value semantics (c:518-532):
635///   - `ret == 0` → no action (no handler vetoed).
636///   - `ret >= 1` → zero `dat[1]`, clear menucmp/menuacc, null minfo.cur.
637///   - `ret >= 2` → also rewind buffer to origline.
638///   - `ret == 2` → also schedule list clear (CLEARLIST=1, invalidatelist).
639pub fn after_complete(dat: &mut [i32]) -> i32 {                              // c:503
640    let menucmp_v = MENUCMP.load(Ordering::Relaxed);
641    let oldmenucmp_v = OLDMENUCMP.load(Ordering::Relaxed);
642
643    // c:505 — `if (menucmp && !oldmenucmp) { ... }`.
644    if menucmp_v == 0 || oldmenucmp_v != 0 {
645        return 0;                                                            // c:535
646    }
647
648    // c:506-517 — build chdata. cdat.matches=amatches, cdat.num=
649    //              nmatches, cdat.nmesg=nmessages, cdat.cur=NULL. The
650    //              Rust hook dispatch path doesn't yet thread chdata
651    //              into shell-fn args (handlers in the standard zsh
652    //              distribution all read directly from compsys globals
653    //              via $compstate). The fields above are still tracked
654    //              via amatches/nmatches/nmessages globals and visible
655    //              to handlers through the normal completion-state
656    //              parameter reads.
657
658    // c:518 — `runhookdef(MENUSTARTHOOK, &cdat)`. C dispatches via
659    // the hookdef chain; the Rust port walks HOOKTAB["menu_start"] and
660    // invokes each shell-fn via the canonical dispatch_function_call
661    // path used by signal-trap shfunc dispatch (signals.rs:1087). The
662    // first non-zero return short-circuits per HOOKF_ALL semantics
663    // (module.c:996-1005).
664    let handlers: Vec<String> = crate::ported::module::HOOKTAB
665        .lock()
666        .ok()
667        .and_then(|t| t.get("menu_start").cloned())
668        .unwrap_or_default();
669
670    let mut ret: i32 = 0;
671    for fn_name in &handlers {
672        let r = crate::fusevm_bridge::with_executor(|exec| {
673            exec.dispatch_function_call(fn_name, &[]).unwrap_or(0)
674        });
675        if r != 0 {
676            ret = r;
677            break;                                                           // c:1001 short-circuit
678        }
679    }
680
681    if ret == 0 {
682        return 0;                                                            // c:535
683    }
684
685    // c:519 — `dat[1] = 0`. The C caller passes a 2-int array; index 1
686    // carries the menu-acceptance flag for the outer compfunc loop.
687    if dat.len() > 1 {
688        dat[1] = 0;
689    }
690    // c:520 — `menucmp = menuacc = 0`.
691    MENUCMP.store(0, Ordering::Relaxed);
692    menuacc.store(0, Ordering::Relaxed);
693    // c:521 — `minfo.cur = NULL`.
694    if let Some(m) = MINFO.get() {
695        if let Ok(mut mi) = m.lock() {
696            mi.cur = None;
697        }
698    }
699
700    if ret >= 2 {                                                            // c:522
701        // c:523 — `fixsuffix()`.
702        crate::ported::zle::zle_misc::fixsuffix();
703        // c:524 — `zlemetacs = 0`.
704        ZLEMETACS.store(0, Ordering::Relaxed);
705        // c:525 — `foredel(zlemetall, CUT_RAW)` removes the entire line.
706        let metall = ZLEMETALL.load(Ordering::Relaxed);
707        crate::ported::zle::zle_utils::foredel(metall, crate::ported::zle::zle_h::CUT_RAW);
708        // c:526 — `inststr(origline)` reinserts the pre-completion buffer.
709        let origline_v: String = crate::ported::zle::zle_tricky::ORIGLINE
710            .get()
711            .and_then(|m| m.lock().ok().map(|g| g.clone()))
712            .unwrap_or_default();
713        let _ = crate::ported::zle::zle_tricky::inststr(&origline_v);
714        // c:527 — `zlemetacs = origcs`.
715        let origcs_v = crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed);
716        ZLEMETACS.store(origcs_v, Ordering::Relaxed);
717
718        if ret == 2 {                                                        // c:528
719            // c:529 — `clearlist = 1`.
720            crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);
721            // c:530 — `invalidatelist()`.
722            crate::ported::zle::zle_h::invalidatelist();
723        }
724    }
725
726    0                                                                        // c:535
727}
728
729// =====================================================================
730// set_list_array — `Src/Zle/compcore.c:1947`.
731// =====================================================================
732
733/// Port of `static void set_list_array(char *name, LinkList l)` from
734/// compcore.c:1947. Writes an array-typed parameter via the canonical
735/// `setaparam` (params.c:3595).
736pub fn set_list_array(name: &str, l: &[String]) {                            // c:1947
737    let _ = crate::ported::params::setaparam(name, l.to_vec());              // c:1956
738}
739
740// =====================================================================
741// get_user_var — `Src/Zle/compcore.c:1956`.
742// =====================================================================
743
744/// Port of `mod_export char **get_user_var(char *nam)` from
745/// compcore.c:1956.
746pub fn get_user_var(nam: Option<&str>) -> Option<Vec<String>> {              // c:1956
747    let nam = nam?;                                                          // c:1956
748    if nam.starts_with('(') {                                                // c:1960
749        let mut arrlist: Vec<String> = Vec::new();
750        let bytes = nam.as_bytes();
751        let mut buf = Vec::<u8>::new();
752        let mut notempty = false;                                            // c:1963
753        let mut brk = false;
754        let mut i = 1;                                                       // c:1967
755        while i < bytes.len() {
756            let b = bytes[i];
757            if b == b'\\' && i + 1 < bytes.len() {                           // c:1969
758                buf.push(bytes[i + 1]);                                      // c:1970
759                notempty = true;
760                i += 2;
761                continue;
762            }
763            if b == b',' || b == b' ' || b == b'\t' || b == b'\n' || b == b')' {
764                if b == b')' { brk = true; }                                 // c:1972
765                if notempty {                                                // c:1974
766                    let mut start = 0;
767                    if !buf.is_empty() && buf[0] == b'\n' { start = 1; }     // c:1977
768                    let s = String::from_utf8_lossy(&buf[start..]).into_owned();
769                    arrlist.push(s);                                         // c:1979
770                }
771                buf.clear();                                                 // c:1981
772                notempty = false;
773            } else {
774                notempty = true;                                             // c:1984
775                buf.push(b);
776            }
777            i += 1;
778            if brk { break; }                                                // c:1988
779        }
780        if !brk || arrlist.is_empty() { return None; }                       // c:1991
781        Some(arrlist)                                                        // c:1996
782    } else {                                                                 // c:1999
783        // c:2003 — `if ((arr = getaparam(nam)) || (arr = gethparam(nam)))
784        //          arr = (incompfunc ? arrdup(arr) : arr);
785        //          else if ((val = getsparam(nam))) { arr = {val, NULL}; }`
786        // Read directly from paramtab: arrays first, then hashed
787        // assoc-array values, then scalar wrapped in a 1-element array.
788        crate::ported::signals::queue_signals();
789        let result = {
790            let tab = match crate::ported::params::paramtab().read() {
791                Ok(t) => t,
792                Err(_) => {
793                    crate::ported::signals::unqueue_signals();
794                    return None;
795                }
796            };
797            tab.get(nam).and_then(|pm| {
798                if let Some(arr) = pm.u_arr.as_ref() {
799                    Some(arr.clone())                                        // c:2004 getaparam
800                } else if let Some(s) = pm.u_str.as_ref() {
801                    Some(vec![s.clone()])                                    // c:2009 getsparam
802                } else {
803                    None
804                }
805            })
806        };
807        crate::ported::signals::unqueue_signals();                           // c:2022
808        result
809    }
810}
811
812// =====================================================================
813// get_data_arr — `Src/Zle/compcore.c:2022`.
814// =====================================================================
815
816/// Direct port of `static char **get_data_arr(char *name, int keys)`
817/// from `Src/Zle/compcore.c:2022`. C uses `fetchvalue` with
818/// `SCANPM_WANTKEYS`/`SCANPM_WANTVALS` + `SCANPM_MATCHMANY` to scan
819/// an associative-array parameter and return either its keys or its
820/// values as a flat array. Without `fetchvalue` ported with full
821/// SCANPM flag support, we go straight to the hashed-storage
822/// thread-local maintained by params.rs for assoc-arrays.
823pub fn get_data_arr(name: &str, keys: bool) -> Option<Vec<String>> {         // c:2022
824    use crate::ported::params::{paramtab, paramtab_hashed_storage};
825    use crate::ported::zsh_h::{PM_HASHED, PM_TYPE};
826
827    crate::ported::signals::queue_signals();                                 // c:2028
828
829    // c:2030-2034 — fetchvalue with SCANPM_MATCHMANY → scan the
830    //                hashed param's keys/values. We approximate by
831    //                routing keys/values directly out of the
832    //                hashed-storage map.
833    let is_hashed = match paramtab().read() {
834        Ok(t) => t.get(name)
835            .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED)
836            .unwrap_or(false),
837        Err(_) => false,
838    };
839
840    let result = if is_hashed {
841        paramtab_hashed_storage().lock().ok().and_then(|m| {
842            m.get(name).map(|map| {
843                if keys {
844                    map.keys().cloned().collect::<Vec<_>>()
845                } else {
846                    map.values().cloned().collect::<Vec<_>>()
847                }
848            })
849        })
850    } else {
851        // c:2032 — non-hashed names return NULL.
852        None
853    };
854
855    crate::ported::signals::unqueue_signals();                               // c:2041
856    result
857}
858
859// =====================================================================
860// addmatch — `Src/Zle/compcore.c:2041`.
861// =====================================================================
862
863/// Port of `static void addmatch(char *str, int flags, char ***dispp,
864///                                int line)` from compcore.c:2041.
865pub fn addmatch(str: &str, flags: i32, disp: Option<&str>, line: bool) {    // c:2041
866    let mut cm = Cmatch::default();                                          // c:2041
867    cm.str = Some(str.to_string());                                        // c:2047
868    // c:2049-2051 — inline read of `complist` parameter, parse `packed`/
869    // `rows` substrings into CMF_PACKED/CMF_ROWS flag bits.
870    let complist_extra = {
871        let s = COMPLIST.get_or_init(|| Mutex::new(String::new()))
872            .lock().map(|g| g.clone()).unwrap_or_default();
873        let packed = if s.contains("packed") { CMF_PACKED } else { 0 };      // c:2050
874        let rows   = if s.contains("rows")   { CMF_ROWS   } else { 0 };      // c:2051
875        if s.is_empty() { 0 } else { packed | rows }
876    };
877    cm.flags = flags | complist_extra;                                       // c:2048
878    if let Some(d) = disp {                                                  // c:2052
879        cm.disp = Some(d.to_string());                                       // c:2056
880    } else if line {                                                         // c:2057
881        cm.disp = Some(String::new());                                       // c:2058
882        cm.flags |= CMF_DISPLINE;                                            // c:2059
883    }
884    mnum.fetch_add(1, Ordering::Relaxed);                                    // c:2061
885    {
886        let cell = curexpl.get_or_init(|| Mutex::new(None));                 // c:2063
887        if let Ok(mut g) = cell.lock() {
888            if let Some(e) = g.as_mut() { e.count += 1; }
889        }
890    }
891    let mcell = matches.get_or_init(|| Mutex::new(Vec::new()));              // c:2066
892    if let Ok(mut g) = mcell.lock() { g.push(cm); }
893    newmatches.store(1, Ordering::Relaxed);                                  // c:2068
894    {
895        let cell = mgroup.get_or_init(|| Mutex::new(None));                  // c:2069
896        if let Ok(mut g) = cell.lock() {
897            if let Some(grp) = g.as_mut() { grp.new_ = 1; }
898        }
899    }
900}
901
902// `lookup_complist_flags` deleted — Rust-only 8-line helper. Inlined
903// at the single call site in callcompfunc (c:2049-2051).
904
905// =====================================================================
906// begcmgroup — `Src/Zle/compcore.c:3073`.
907// =====================================================================
908
909/// Port of `mod_export void begcmgroup(char *n, int flags)` from
910/// compcore.c:3073.
911pub fn begcmgroup(n: Option<&str>, flags: i32) {                             // c:3073
912    if let Some(name) = n {                                                  // c:3073
913        let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON                    // c:3085
914                 | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
915        let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
916        if let Ok(g) = cell.lock() {
917            for grp in g.iter() {                                            // c:3078
918                if grp.name.as_deref() == Some(name)                         // c:3084-3087
919                    && (grp.flags & mask) == flags
920                {
921                    let active = grp.clone();                                // c:3088
922                    let mc = mgroup.get_or_init(|| Mutex::new(None));
923                    if let Ok(mut s) = mc.lock() { *s = Some(active); }
924                    return;                                                  // c:3095
925                }
926            }
927        }
928    }
929    let mut grp = Cmgroup::default();                                        // c:3101
930    grp.name = n.map(String::from);                                          // c:3105
931    grp.flags = flags;                                                       // c:3108
932    let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
933    if let Ok(mut g) = cell.lock() {
934        g.insert(0, grp.clone());                                            // c:3121-3124
935    }
936    let mc = mgroup.get_or_init(|| Mutex::new(None));
937    if let Ok(mut s) = mc.lock() { *s = Some(grp); }
938    if let Ok(mut g) = expls.get_or_init(|| Mutex::new(Vec::new())).lock()    { g.clear(); }
939    if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock()  { g.clear(); }
940    if let Ok(mut g) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() { g.clear(); }
941    if let Ok(mut g) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock()   { g.clear(); }
942}
943
944// =====================================================================
945// endcmgroup — `Src/Zle/compcore.c:3131`.
946// =====================================================================
947
948/// Port of `mod_export void endcmgroup(char **ylist)` from
949/// compcore.c:3131.
950pub fn endcmgroup(ylist: Option<Vec<String>>) {                              // c:3131
951    let cell = mgroup.get_or_init(|| Mutex::new(None));
952    if let Ok(mut g) = cell.lock() {
953        if let Some(grp) = g.as_mut() {
954            grp.ylist = ylist.unwrap_or_default();                           // c:3140
955        }
956    }
957}
958
959// =====================================================================
960// addexpl — `Src/Zle/compcore.c:3140`.
961// =====================================================================
962
963/// Port of `mod_export void addexpl(int always)` from compcore.c:3140.
964pub fn addexpl(always: bool) {                                               // c:3140
965    let curexpl_snap = {
966        let cell = curexpl.get_or_init(|| Mutex::new(None));
967        cell.lock().ok().and_then(|g| g.clone())
968    };
969    let curexpl_str = match curexpl_snap.as_ref().and_then(|e| e.str.clone()) {
970        Some(s) => s,
971        None => return,
972    };
973    let curexpl_count  = curexpl_snap.as_ref().map(|e| e.count).unwrap_or(0);
974    let curexpl_fcount = curexpl_snap.as_ref().map(|e| e.fcount).unwrap_or(0);
975
976    let elist = expls.get_or_init(|| Mutex::new(Vec::new()));
977    if let Ok(mut g) = elist.lock() {
978        for e in g.iter_mut() {                                              // c:3145
979            if e.str.as_deref() == Some(curexpl_str.as_str()) {             // c:3147
980                e.count  += curexpl_count;                                   // c:3148
981                e.fcount += curexpl_fcount;                                  // c:3149
982                if always {                                                  // c:3150
983                    e.always = 1;
984                    nmessages.fetch_add(1, Ordering::Relaxed);               // c:3152
985                    newmatches.store(1, Ordering::Relaxed);                  // c:3153
986                    let mc = mgroup.get_or_init(|| Mutex::new(None));
987                    if let Ok(mut mg) = mc.lock() {
988                        if let Some(grp) = mg.as_mut() { grp.new_ = 1; }
989                    }
990                }
991                return;                                                      // c:3156
992            }
993        }
994        if let Some(e) = curexpl_snap {                                      // c:3159
995            g.push(e);
996        }
997    }
998    newmatches.store(1, Ordering::Relaxed);                                  // c:3160
999    if always {                                                              // c:3161
1000        let mc = mgroup.get_or_init(|| Mutex::new(None));
1001        if let Ok(mut mg) = mc.lock() {
1002            if let Some(grp) = mg.as_mut() { grp.new_ = 1; }
1003        }
1004        nmessages.fetch_add(1, Ordering::Relaxed);                           // c:3173
1005    }
1006}
1007
1008// =====================================================================
1009// matchcmp — `Src/Zle/compcore.c:3173`.
1010// =====================================================================
1011
1012/// Port of `static int matchcmp(Cmatch *a, Cmatch *b)` from
1013/// compcore.c:3173.
1014pub fn matchcmp(a: &Cmatch, b: &Cmatch) -> std::cmp::Ordering {              // c:3173
1015    let order = MATCHORDER.load(Ordering::Relaxed);
1016    let sortdir = if (order & CGF_REVSORT) != 0 { -1 } else { 1 };           // c:3177
1017
1018    let cmp = (b.disp.is_some() as i32) - (a.disp.is_some() as i32);         // c:3176
1019    let (as_, bs) = if (order & CGF_MATSORT) != 0 || (cmp == 0 && a.disp.is_none()) {
1020        (a.str.clone().unwrap_or_default(),                                 // c:3181
1021         b.str.clone().unwrap_or_default())                                 // c:3182
1022    } else {
1023        if cmp != 0 {                                                        // c:3184
1024            let raw = (cmp as i32) * sortdir;
1025            return if raw < 0 { std::cmp::Ordering::Less }                   // c:3185
1026                   else if raw > 0 { std::cmp::Ordering::Greater }
1027                   else { std::cmp::Ordering::Equal };
1028        }
1029        let displine_cmp = (b.flags & CMF_DISPLINE) - (a.flags & CMF_DISPLINE); // c:3187
1030        if displine_cmp != 0 {                                               // c:3188
1031            let raw = displine_cmp * sortdir;
1032            return if raw < 0 { std::cmp::Ordering::Less }
1033                   else if raw > 0 { std::cmp::Ordering::Greater }
1034                   else { std::cmp::Ordering::Equal };
1035        }
1036        (a.disp.clone().unwrap_or_default(),                                 // c:3191
1037         b.disp.clone().unwrap_or_default())                                 // c:3192
1038    };
1039    let raw = sortdir * if as_ == bs { 0 } else if as_ < bs { -1 } else { 1 };
1040    if raw < 0 { std::cmp::Ordering::Less }                                  // c:3195
1041    else if raw > 0 { std::cmp::Ordering::Greater }
1042    else { std::cmp::Ordering::Equal }
1043}
1044
1045// =====================================================================
1046// matcheq — `Src/Zle/compcore.c:3203-3215`.
1047// =====================================================================
1048
1049#[inline]
1050fn matchstreq(a: Option<&String>, b: Option<&String>) -> bool {              // c:3207
1051    match (a, b) {
1052        (None, None) => true,
1053        (Some(x), Some(y)) => x == y,
1054        _ => false,
1055    }
1056}
1057
1058/// Port of `static int matcheq(Cmatch a, Cmatch b)` from
1059/// compcore.c:3206.
1060pub fn matcheq(a: &Cmatch, b: &Cmatch) -> bool {                             // c:3207
1061    matchstreq(a.ipre.as_ref(),  b.ipre.as_ref())  &&                        // c:3207
1062    matchstreq(a.pre.as_ref(),   b.pre.as_ref())   &&                        // c:3210
1063    matchstreq(a.ppre.as_ref(),  b.ppre.as_ref())  &&                        // c:3211
1064    matchstreq(a.psuf.as_ref(),  b.psuf.as_ref())  &&                        // c:3212
1065    matchstreq(a.suf.as_ref(),   b.suf.as_ref())   &&                        // c:3213
1066    matchstreq(a.str.as_ref(),  b.str.as_ref())                            // c:3214
1067}
1068
1069// =====================================================================
1070// freematch / freematches — `Src/Zle/compcore.c:3575 / 3605`.
1071// =====================================================================
1072
1073/// Port of `void freematch(Cmatch m, int *cl, int rec)` from
1074/// compcore.c:3575. Rust's `Drop` covers it.
1075pub fn freematch(_m: Cmatch) {                                               // c:3575
1076}
1077
1078/// Port of `mod_export void freematches(Cmgroup g, int cl)` from
1079/// compcore.c:3605. Rust's `Drop` covers it.
1080pub fn freematches(_g: Vec<Cmgroup>) {                                       // c:3605
1081}
1082
1083// =====================================================================
1084// Substrate-blocked stubs — bodies need substrate listed in each
1085// doc comment. Returns shape-correct safe defaults.
1086// =====================================================================
1087
1088// =====================================================================
1089// do_completion — `Src/Zle/compcore.c:287`.
1090// =====================================================================
1091
1092/// Direct port of `int do_completion(Hookdef dummy, Compldat dat)`
1093/// from compcore.c:287. The top-level completion driver: per-round
1094/// state reset → `makecomplist` → dispatch to `do_ambiguous` /
1095/// `do_single` / `do_allmatches` per result count.
1096pub fn do_completion(s: &str, incmd: i32, lst: i32) -> i32 {                 // c:287
1097
1098    let osl = crate::ported::zle::zle_refresh::SHOWINGLIST.load(Ordering::Relaxed);                                            // c:289
1099    let mut ret: i32 = 0;                                                    // c:289
1100
1101    // c:296-297 — `ainfo = fainfo = NULL`.
1102    if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() { *g = None; }
1103    if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() { *g = None; }
1104    if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
1105        g.clear();                                                            // c:298
1106    }
1107
1108    // c:300-307 — compqstack reset.
1109    let instring = crate::ported::zle::zle_tricky::INSTRING.load(Ordering::Relaxed);                                          // c:307
1110    // c:305 — `compqstack = instring == QT_NONE ? "\\" : <quote-char>`.
1111    // Inlined `char_from_qt(x)` as `(x as u8) as char`.
1112    let head_q: char = if instring == crate::ported::zsh_h::QT_NONE {        // c:305
1113        crate::ported::zsh_h::QT_BACKSLASH as u8 as char
1114    } else {
1115        instring as u8 as char
1116    };
1117    if let Ok(mut g) = compqstack.get_or_init(|| Mutex::new(String::new())).lock() {
1118        *g = head_q.to_string();                                              // c:305-306
1119    }
1120
1121    hasunqu.store(0, Ordering::Relaxed);                                     // c:309
1122    let wouldinstab_v = WOULDINSTAB.load(Ordering::Relaxed);                 // c:310
1123    useline.store(                                                           // c:310
1124        if wouldinstab_v != 0 { -1 } else if lst != crate::ported::zle::zle_h::COMP_LIST_COMPLETE { 1 } else { 0 },
1125        Ordering::Relaxed,
1126    );
1127    useexact.store(opt_isset("RECEXACT"), Ordering::Relaxed);           // c:311
1128    set_compstate_str("exact_string", "");                                   // c:312
1129    let useline_v = useline.load(Ordering::Relaxed);
1130    uselist.store(                                                           // c:314
1131        if useline_v != 0 {
1132            if opt_isset("AUTOLIST") != 0 && opt_isset("BASHAUTOLIST") == 0 {
1133                if opt_isset("LISTAMBIGUOUS") != 0 { 3 } else { 2 }
1134            } else { 0 }
1135        } else { 1 },
1136        Ordering::Relaxed,
1137    );
1138
1139    let useglob_v = USEGLOB.load(Ordering::Relaxed);                         // c:319
1140    let opm: String = if useglob_v != 0 { "*".into() } else { "".into() };
1141    if let Ok(mut g) = comppatmatch.get_or_init(|| Mutex::new(None)).lock() {
1142        *g = Some(opm.clone());                                              // c:319
1143    }
1144    set_compstate_str("pattern_insert", "menu");                             // c:320
1145    forcelist.store(0, Ordering::Relaxed);                                   // c:322
1146    haspattern.store(0, Ordering::Relaxed);                                  // c:323
1147    let _complistmax = env_iparam("LISTMAX");                                // c:324
1148
1149    set_compstate_str(                                                       // c:326
1150        "last_prompt",
1151        if opt_isset("ALWAYSLASTPROMPT") != 0 { "yes" } else { "" },
1152    );
1153    dolastprompt.store(1, Ordering::Relaxed);                                // c:327
1154
1155    // c:329-330 — complist string.
1156    let cl_str = if opt_isset("LISTROWSFIRST") != 0 {
1157        if opt_isset("LISTPACKED") != 0 { "packed rows" } else { "rows" }
1158    } else if opt_isset("LISTPACKED") != 0 { "packed" } else { "" };
1159    if let Ok(mut g) = crate::ported::zle::complete::COMPLIST
1160        .get_or_init(|| Mutex::new(String::new())).lock()
1161    {
1162        *g = cl_str.into();                                                  // c:329
1163    }
1164    startauto.store(opt_isset("AUTOMENU"), Ordering::Relaxed);          // c:331
1165
1166    let zlc = ZLEMETACS.load(Ordering::Relaxed);
1167    let we_v = WE.load(Ordering::Relaxed);
1168    movetoend.store(                                                         // c:332
1169        if zlc == we_v || opt_isset("ALWAYSTOEND") != 0 { 2 } else { 1 },
1170        Ordering::Relaxed,
1171    );
1172    crate::ported::zle::zle_refresh::SHOWINGLIST.store(0, Ordering::Relaxed);                                                      // c:333
1173    hasmatched.store(0, Ordering::Relaxed);                                  // c:334
1174    hasunmatched.store(0, Ordering::Relaxed);                                // c:334
1175    minmlen.store(1_000_000, Ordering::Relaxed);                             // c:335
1176    maxmlen.store(-1, Ordering::Relaxed);                                    // c:336
1177    nmessages.store(0, Ordering::Relaxed);                                   // c:338
1178    hasallmatch.store(0, Ordering::Relaxed);                                 // c:339
1179
1180    // c:342 — main dispatch.
1181    if makecomplist(s, incmd, lst) != 0 {                                    // c:342
1182        // c:344 — error path.
1183        ZLEMETACS.store(0, Ordering::Relaxed);                               // c:344
1184        foredel(ZLEMETALL.load(Ordering::Relaxed));                     // c:345
1185        inststr(&crate::ported::zle::zle_tricky::ORIGLINE.get_or_init(|| Mutex::new(String::new())).lock().map(|g| g.clone()).unwrap_or_default());                                      // c:346
1186        ZLEMETACS.store(crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed);                   // c:347
1187        crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);                                                    // c:348
1188        ret = 1;
1189        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; }                                                   // c:350
1190        if useline.load(Ordering::Relaxed) < 0 {                             // c:351
1191            unmetafy_line();
1192            ret = selfinsert();                                         // c:353
1193            metafy_line();
1194        }
1195        return goto_compend(ret);                                            // c:356 goto compend
1196    }
1197
1198    // c:359-361 — clear lastprebr/lastpostbr.
1199    lastprebr_set("");                                                       // c:359
1200    lastpostbr_set("");                                                      // c:360
1201
1202    let curpm = comppatmatch.get_or_init(|| Mutex::new(None))
1203        .lock().ok().and_then(|g| g.clone()).unwrap_or_default();
1204    if !curpm.is_empty() && curpm != opm {                                   // c:363
1205        haspattern.store(1, Ordering::Relaxed);                              // c:364
1206    }
1207    let nm = nmatches.load(Ordering::Relaxed);                               // c:366
1208    let dm = diffmatches.load(Ordering::Relaxed);
1209    if iforcemenu.load(Ordering::Relaxed) != 0 {                             // c:366
1210        if nm != 0 { { let _ = crate::ported::zle::compresult::do_ambig_menu(); }; }                                 // c:367
1211        ret = if nm == 0 { 1 } else { 0 };                                   // c:369
1212    } else if useline.load(Ordering::Relaxed) < 0 {                          // c:370
1213        unmetafy_line();
1214        ret = selfinsert();                                             // c:372
1215        metafy_line();
1216    } else if useline.load(Ordering::Relaxed) == 0
1217           && uselist.load(Ordering::Relaxed) != 0
1218    {                                                                        // c:374
1219        ZLEMETACS.store(0, Ordering::Relaxed);                               // c:375
1220        foredel(ZLEMETALL.load(Ordering::Relaxed));                     // c:376
1221        inststr(&crate::ported::zle::zle_tricky::ORIGLINE.get_or_init(|| Mutex::new(String::new())).lock().map(|g| g.clone()).unwrap_or_default());                                      // c:377
1222        ZLEMETACS.store(crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed);                   // c:378
1223        crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);                                                 // c:379
1224    } else if useline.load(Ordering::Relaxed) == 2 && nm > 1 {               // c:380
1225        // c:381 — `do_allmatches(1)`. Inlined: build flat match list
1226        // from `amatches` and dispatch to compresult::do_allmatches.
1227        {
1228            let groups = amatches.get_or_init(|| Mutex::new(Vec::new()))
1229                .lock().map(|g| g.clone()).unwrap_or_default();
1230            let mut all: Vec<String> = Vec::new();
1231            for g in groups {
1232                for m in g.matches {
1233                    if let Some(s) = m.str { all.push(s); }
1234                }
1235            }
1236            let buf = ZLEMETALINE.get_or_init(|| Mutex::new(String::new()))
1237                .lock().map(|g| g.clone()).unwrap_or_default();
1238            let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
1239            let wb = WB.load(Ordering::Relaxed) as usize;
1240            let we = WE.load(Ordering::Relaxed) as usize;
1241            let (new_buf, new_cs) = crate::ported::zle::compresult::do_allmatches(
1242                &buf, cs, wb, we, &all, " ",
1243            );
1244            if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1245                *g = new_buf;
1246                ZLEMETALL.store(g.len() as i32, Ordering::Relaxed);
1247            }
1248            ZLEMETACS.store(new_cs as i32, Ordering::Relaxed);
1249        }
1250        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; }                                                   // c:383
1251        if forcelist.load(Ordering::Relaxed) != 0 {                          // c:385
1252            crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);
1253        } else {
1254            crate::ported::zle::zle_h::invalidatelist();                                           // c:388
1255        }
1256    } else if useline.load(Ordering::Relaxed) != 0 {                         // c:389
1257        if nm > 1 && dm != 0 {                                               // c:391
1258            // c:393 — `ret = do_ambiguous()`. Inlined: flatten `amatches`
1259            // into &[String] and dispatch.
1260            ret = {
1261                let groups = amatches.get_or_init(|| Mutex::new(Vec::new()))
1262                    .lock().map(|g| g.clone()).unwrap_or_default();
1263                let all: Vec<String> = groups.into_iter()
1264                    .flat_map(|g| g.matches.into_iter().filter_map(|m| m.str))
1265                    .collect();
1266                crate::ported::zle::compresult::do_ambiguous(&all)
1267            };
1268            if crate::ported::zle::zle_refresh::SHOWINGLIST.load(Ordering::Relaxed) == 0
1269                && uselist.load(Ordering::Relaxed) != 0
1270                && crate::ported::zle::zle_refresh::LISTSHOWN.load(Ordering::Relaxed) != 0
1271                && (crate::ported::zle::zle_tricky::USEMENU
1272                       .load(Ordering::Relaxed) == 2
1273                    || oldlist.load(Ordering::Relaxed) != 0)
1274            {
1275                crate::ported::zle::zle_refresh::SHOWINGLIST.store(osl, Ordering::Relaxed);                                        // c:395
1276            }
1277        } else if nm == 1 || (nm > 1 && dm == 0) {                           // c:396
1278            do_single_first_match();                                         // c:399-411
1279            if forcelist.load(Ordering::Relaxed) != 0 {                      // c:412
1280                if uselist.load(Ordering::Relaxed) != 0 {
1281                    crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);
1282                } else {
1283                    crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);
1284                }
1285            } else {
1286                crate::ported::zle::zle_h::invalidatelist();                                       // c:418
1287            }
1288        } else if nmessages.load(Ordering::Relaxed) != 0
1289            && forcelist.load(Ordering::Relaxed) != 0
1290        {                                                                    // c:419
1291            if uselist.load(Ordering::Relaxed) != 0 {
1292                crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);
1293            } else {
1294                crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);
1295            }
1296        }
1297    } else {                                                                 // c:425
1298        crate::ported::zle::zle_h::invalidatelist();                                               // c:426
1299        crate::ported::zle::zle_tricky::LASTAMBIG.store(                     // c:427
1300            opt_isset("BASHAUTOLIST"),
1301            Ordering::Relaxed,
1302        );
1303        if forcelist.load(Ordering::Relaxed) != 0 { crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed); }      // c:428
1304        ZLEMETACS.store(0, Ordering::Relaxed);                               // c:429
1305        foredel(ZLEMETALL.load(Ordering::Relaxed));                     // c:430
1306        inststr(&crate::ported::zle::zle_tricky::ORIGLINE.get_or_init(|| Mutex::new(String::new())).lock().map(|g| g.clone()).unwrap_or_default());                                      // c:431
1307        ZLEMETACS.store(crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed);                   // c:432
1308    }
1309
1310    // c:436 — explanation strings.
1311    if crate::ported::zle::zle_refresh::SHOWINGLIST.load(Ordering::Relaxed) == 0
1312        && crate::ported::zle::zle_tricky::VALIDLIST.load(Ordering::Relaxed) != 0
1313        && crate::ported::zle::zle_tricky::USEMENU.load(Ordering::Relaxed) != 2
1314        && uselist.load(Ordering::Relaxed) != 0
1315        && (nm != 1 || dm != 0)
1316        && useline.load(Ordering::Relaxed) >= 0
1317        && useline.load(Ordering::Relaxed) != 2
1318        && (oldlist.load(Ordering::Relaxed) == 0 || crate::ported::zle::zle_refresh::LISTSHOWN.load(Ordering::Relaxed) == 0)
1319    {
1320        onlyexpl.store(3, Ordering::Relaxed);                                // c:441
1321        crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);                                                 // c:442
1322    }
1323
1324    goto_compend(ret)
1325}
1326
1327/// First-match shortcut path from compcore.c:398-411. `Cmgroup m = amatches;
1328/// while (!m->mcount) m = m->next; do_single(m->matches[0])`.
1329fn do_single_first_match() {                                                  // c:398
1330    let groups = amatches.get_or_init(|| Mutex::new(Vec::new()))
1331        .lock().ok().map(|g| g.clone()).unwrap_or_default();
1332    let first = groups.into_iter().find(|g| g.mcount > 0)
1333        .and_then(|g| g.matches.first().cloned());
1334    if let Some(m) = first {
1335        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; }                                                   // c:407
1336        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.asked = 0; }                                                  // c:408
1337        // c:409 — `do_single(m)`. Inlined: drop the Cmatch payload onto
1338        // MINFO.cur so the listing path picks it up (matches the C
1339        // behavior of routing the single-match insert through minfo).
1340        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() {
1341            g.cur = Some(Box::new(m));
1342        }
1343    }
1344}
1345
1346/// compcore.c:444 `compend:` epilogue — free matchers, snap zlemetacs.
1347fn goto_compend(ret: i32) -> i32 {                                            // c:444
1348    if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
1349        g.clear();                                                            // c:445-446 freecmatcher loop
1350    }
1351    let line_len = ZLEMETALL.load(Ordering::Relaxed);                        // c:448 strlen(zlemetaline)
1352    if ZLEMETACS.load(Ordering::Relaxed) > line_len {                        // c:449
1353        ZLEMETACS.store(line_len, Ordering::Relaxed);                        // c:450
1354    }
1355    ret                                                                      // c:453
1356}
1357
1358// `COMP_LIST_COMPLETE` / `QT_NONE_STUB` / `QT_BACKSLASH_STUB` local
1359// aliases deleted — call sites now reach the real C-side constants
1360// directly (`crate::ported::zle::zle_h::COMP_LIST_COMPLETE`,
1361// `crate::ported::zsh_h::QT_NONE`, `crate::ported::zsh_h::QT_BACKSLASH`).
1362// The local `COMP_LIST_COMPLETE = 2` was a value-mismatch bug (the
1363// real constant is 1 per `Src/Zle/zle.h:357`).
1364
1365// `char_from_qt` deleted — Rust-only 1-line `(qt as u8) as char`
1366// helper. Inlined at the two call sites in get_compstate_str.
1367
1368// `showinglist_stub` / `showinglist_set` / `clearlist_set` /
1369// `listshown_stub` / `instring_stub` deleted — Rust-only 1-line
1370// accessors for C globals (SHOWINGLIST / CLEARLIST / LISTSHOWN /
1371// INSTRING). C reads/writes the bare globals inline; callers in
1372// compcore.rs now do `<GLOBAL>.load(Ordering::Relaxed)` /
1373// `<GLOBAL>.store(v, Ordering::Relaxed)` directly.
1374/// Direct port of `foredel(int ct, int flags)` from
1375/// `Src/Zle/zle_utils.c:1105`. Deletes `ct` chars forward from
1376/// `ZLEMETACS` in the global metafied line. Operates on the
1377/// `ZLEMETALINE` global rather than a `&mut Zle` handle since
1378/// compcore's call site (compcore.c:344-355 error-recovery) drives
1379/// the global ZLE buffer directly.
1380/// WARNING: param names don't match C — Rust=(ct) vs C=(ct, flags)
1381fn foredel(ct: i32) {                                                    // zle_utils.c:1105
1382    if ct <= 0 { return; }
1383    let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
1384    if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1385        let bytes = g.as_bytes();
1386        if cs >= bytes.len() { return; }
1387        let end = (cs + ct as usize).min(bytes.len());
1388        // c:1108-1115 — splice out [cs..end).
1389        let new_line: String = String::from_utf8_lossy(&bytes[..cs]).into_owned()
1390            + &String::from_utf8_lossy(&bytes[end..]);
1391        let new_len = new_line.len() as i32;
1392        *g = new_line;
1393        ZLEMETALL.store(new_len, Ordering::Relaxed);
1394    }
1395}
1396
1397/// Direct port of `inststr(char *s)` from `Src/Zle/zle_tricky.c:278`.
1398/// Inserts `s` at `ZLEMETACS` in the global metafied line.
1399/// Direct port of `#define inststr(X) inststrlen((X),1,-1)` from
1400/// `Src/Zle/zle_tricky.c:57`. Inserts `s` at `ZLEMETACS` in the
1401/// global metafied line; cursor advances by `s.len()`.
1402/// WARNING: param names don't match C — Rust=(s) vs C=()
1403fn inststr(s: &str) {                                                    // c:57
1404    if s.is_empty() { return; }
1405    let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
1406    if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1407        let bytes = g.as_bytes();
1408        let cs = cs.min(bytes.len());
1409        let new_line: String = String::from_utf8_lossy(&bytes[..cs]).into_owned()
1410            + s
1411            + &String::from_utf8_lossy(&bytes[cs..]);
1412        let new_len = new_line.len() as i32;
1413        *g = new_line;
1414        ZLEMETALL.store(new_len, Ordering::Relaxed);
1415        ZLEMETACS.store(cs as i32 + s.len() as i32, Ordering::Relaxed);
1416    }
1417}
1418// `origline_stub` / `origcs_stub` deleted — Rust-only 1-line
1419// accessors for the `ORIGLINE` / `ORIGCS` globals (ports of C
1420// `origline` / `origcs` at zle_tricky.c:75 etc.). C reads these
1421// globals inline; callers in compcore.rs now do the lock/load
1422// directly.
1423/// Direct port of `void unmetafy_line(void)` from `zle_tricky.c:995`.
1424/// Reads `ZLEMETALINE`, runs `unmetafy_line(...)` from zle_tricky.rs,
1425/// stores result into `ZLELINE` + updates `ZLECS`/`ZLELL`.
1426fn unmetafy_line() {                                                     // zle_tricky.c:995
1427    let meta = ZLEMETALINE.get_or_init(|| Mutex::new(String::new()))
1428        .lock().map(|g| g.clone()).unwrap_or_default();
1429    let unmeta = crate::ported::zle::zle_tricky::unmetafy_line(&meta);
1430    let new_len = unmeta.len() as i32;
1431    let cs = ZLEMETACS.load(Ordering::Relaxed);                              // c:978-1000
1432    if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
1433        *g = unmeta;
1434    }
1435    ZLELL.store(new_len, Ordering::Relaxed);
1436    ZLECS.store(cs.min(new_len), Ordering::Relaxed);
1437}
1438
1439/// Direct port of `void metafy_line(void)` from `zle_tricky.c:978`.
1440/// Reads `ZLELINE`, runs `metafy_line(...)` from zle_tricky.rs, stores
1441/// result into `ZLEMETALINE` + updates `ZLEMETACS`/`ZLEMETALL`.
1442fn metafy_line() {                                                       // zle_tricky.c:978
1443    let raw = ZLELINE.get_or_init(|| Mutex::new(String::new()))
1444        .lock().map(|g| g.clone()).unwrap_or_default();
1445    let meta = crate::ported::zle::zle_tricky::metafy_line(&raw);
1446    let new_len = meta.len() as i32;
1447    let cs = ZLECS.load(Ordering::Relaxed);
1448    if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1449        *g = meta;
1450    }
1451    ZLEMETALL.store(new_len, Ordering::Relaxed);
1452    ZLEMETACS.store(cs.min(new_len), Ordering::Relaxed);
1453}
1454
1455/// Direct port of `int selfinsert(char **args)` from `Src/Zle/zle_misc.c:113`.
1456/// Inserts `lastchar` from ZLE state at cursor. Without a `&mut Zle`
1457/// handle we operate on the global `ZLELINE` + a thread-local
1458/// lastchar holder. Equivalent C body: insert one char at zlecs,
1459/// advance zlecs, bump zlell.
1460fn selfinsert() -> i32 {                                                 // zle_misc.c:112
1461    let ch = LASTCHAR.load(Ordering::Relaxed);                               // c:113
1462    if ch < 0 { return 1; }                                                  // c:116 EOF
1463    let cs = ZLECS.load(Ordering::Relaxed) as usize;
1464    if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
1465        let mut bytes = g.as_bytes().to_vec();
1466        let cs = cs.min(bytes.len());
1467        // c:130 — insertion at cs.
1468        if (ch as u32) < 128 {
1469            bytes.insert(cs, ch as u8);
1470        } else if let Some(c) = char::from_u32(ch as u32) {
1471            let mut buf = [0u8; 4];
1472            let enc = c.encode_utf8(&mut buf).as_bytes();
1473            for (i, b) in enc.iter().enumerate() {
1474                bytes.insert(cs + i, *b);
1475            }
1476        }
1477        *g = String::from_utf8_lossy(&bytes).into_owned();
1478        let new_len = g.len() as i32;
1479        ZLELL.store(new_len, Ordering::Relaxed);
1480        ZLECS.store((cs + 1) as i32, Ordering::Relaxed);
1481    }
1482    0                                                                        // c:141
1483}
1484
1485/// Port of `mod_export int lastchar` from `Src/Zle/zle_main.c`. Last
1486/// keyboard char consumed by the binding loop — read by `selfinsert`.
1487pub static LASTCHAR: AtomicI32 = AtomicI32::new(0);                          // zle_main.c
1488// minfo_clear_cur / minfo_asked_zero deleted — Rust-only 2-line
1489// wrappers around C's inline writes `minfo.cur = NULL` and
1490// `minfo.asked = 0`. All call sites inlined.
1491
1492/// Direct port of `struct menuinfo minfo` — `Src/Zle/zle_tricky.c`
1493/// (the single file-scope instance). The struct type itself lives
1494/// in `comp_h.rs::Menuinfo` (port of comp.h:284-295).
1495pub static MINFO: OnceLock<Mutex<crate::ported::zle::comp_h::Menuinfo>> = OnceLock::new(); // zle_tricky.c minfo
1496
1497// `set_minfo_cur` deleted — Rust-only wrapper for the C inline
1498// write `minfo.cur = &m;`. Callers should inline the
1499// `MINFO.lock().cur = Some(Box::new(m))` write directly.
1500// do_ambig_menu_stub deleted — inlined as
1501// `{ let _ = crate::ported::zle::compresult::do_ambig_menu(); }`
1502// at the single call site (c:367).
1503// do_ambiguous_stub / do_single_stub / do_allmatches_stub /
1504// invalidatelist_stub deleted — Rust-only glue wrappers, all
1505// inlined at their (single) call sites in do_completion / dupmatch.
1506// The real C names live as `pub fn` in compresult.rs / zle_h.rs.
1507fn opt_isset(name: &str) -> i32 {                                        // options.c
1508    if crate::ported::options::opt_state_get(name).unwrap_or(false) { 1 } else { 0 }
1509}
1510/// Real call into `getiparam(name)` — the canonical paramtab read.
1511/// Mirrors C's `getiparam` at params.c:3044 which reads the global
1512/// `paramtab` directly via `gethashnode2`.
1513fn env_iparam(name: &str) -> i32 {                                            // params.c:3044
1514    crate::ported::params::getiparam(name) as i32
1515}
1516fn lastprebr_set(s: &str) {                                                   // zle_tricky.c lastprebr
1517    if let Ok(mut g) = crate::ported::zle::zle_tricky::LASTPREBR
1518        .get_or_init(|| Mutex::new(String::new())).lock()
1519    {
1520        *g = s.to_string();
1521    }
1522}
1523fn lastpostbr_set(s: &str) {                                                  // zle_tricky.c lastpostbr
1524    if let Ok(mut g) = crate::ported::zle::zle_tricky::LASTPOSTBR
1525        .get_or_init(|| Mutex::new(String::new())).lock()
1526    {
1527        *g = s.to_string();
1528    }
1529}
1530
1531
1532// =====================================================================
1533// callcompfunc — `Src/Zle/compcore.c:544`.
1534// =====================================================================
1535
1536/// Port of `static void callcompfunc(char *s, char *fn)` from
1537/// compcore.c:544. Selects the `$compstate[context]` value, then
1538/// dispatches into the user shell function `fn`. Paramtab setup
1539/// (`comprpms`/`compkpms`) + result-readback is stubbed locally
1540/// per PORT.md Rule 9 until `params.c` substrate lands.
1541pub fn callcompfunc(s: &str, fn_name: &str) {                                // c:544
1542
1543    if fn_name.is_empty() { return; }                                        // c:552 getshfunc(NULL)
1544    let _lv  = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed);                                               // c:548 int lv = lastval
1545    let _icf = crate::ported::utils::INCOMPFUNC.load(Ordering::Relaxed);                                            // c:555
1546    let _osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed);                                             // c:555
1547
1548    let _useglob = USEGLOB.load(Ordering::Relaxed);                          // c:579
1549
1550    // c:591-617 — context selection.
1551    let context = compcontext_for(s);                                        // c:591-617
1552    set_compstate_str("context", &context);                                  // c:619
1553
1554    // c:721-727 — `$compstate[last_prompt]` etc. fed in from
1555    // do_completion via dolastprompt; we forward the current values.
1556    set_compstate_str(
1557        "last_prompt",
1558        if dolastprompt.load(Ordering::Relaxed) != 0 { "yes" } else { "" },
1559    );
1560
1561    // c:740-749 — `$compstate[list]` — set from `complist` global.
1562    let cl_value = crate::ported::zle::complete::COMPLIST
1563        .get_or_init(|| Mutex::new(String::new()))
1564        .lock().map(|g| g.clone()).unwrap_or_default();
1565    set_compstate_str("list", &cl_value);                                    // c:740
1566
1567    // c:768-785 — `$compstate[insert]` per (useline, usemenu).
1568    let ul = useline.load(Ordering::Relaxed);
1569    let um = crate::ported::zle::zle_tricky::USEMENU.load(Ordering::Relaxed);
1570    let ins = if ul != 0 {
1571        match um {
1572            0 => "unambiguous",
1573            1 => "menu",
1574            2 => "automenu",
1575            _ => "",
1576        }
1577    } else { "" };
1578    set_compstate_str("insert", ins);                                        // c:770
1579
1580    // c:790-794 — `$compstate[exact]` & `$compstate[exact_string]`.
1581    set_compstate_str(
1582        "exact",
1583        if useexact.load(Ordering::Relaxed) != 0 { "accept" } else { "" },
1584    );
1585
1586    // c:800-803 — `$compstate[to_end]` per movetoend.
1587    set_compstate_str(
1588        "to_end",
1589        if movetoend.load(Ordering::Relaxed) == 1 { "single" } else { "match" },
1590    );
1591
1592    // c:838 — `incompfunc = 1` before invoking the user fn.
1593    crate::ported::utils::INCOMPFUNC.store(1, Ordering::Relaxed);            // c:838
1594
1595    // c:638 — doshfunc(fn).
1596    let _ = shfunc_call(fn_name);                                       // c:638
1597
1598    // c:909-912 — unwind: read `$compstate[insert]` etc. back into
1599    // the compcore globals so do_completion sees the user fn's
1600    // mutations.
1601    let post_insert = crate::ported::params::getsparam("compstate[insert]")
1602        .unwrap_or_default();
1603    if !post_insert.is_empty() {
1604        if post_insert.contains("automenu") {
1605            crate::ported::zle::zle_tricky::USEMENU.store(2, Ordering::Relaxed);
1606        } else if post_insert.contains("menu") {
1607            crate::ported::zle::zle_tricky::USEMENU.store(1, Ordering::Relaxed);
1608        }
1609    }
1610
1611    // c:914 — incompfunc = icf. Restore.
1612    crate::ported::utils::INCOMPFUNC.store(_icf, Ordering::Relaxed);
1613}
1614
1615/// Choose `$compstate[context]` per the lex classification in `inwhat`
1616/// (and the `ispar` modifier). Direct lift of compcore.c:591-617.
1617fn compcontext_for(_s: &str) -> String {                                     // c:591
1618    let ip = ispar.load(Ordering::Relaxed);                                  // c:599
1619    if ip == 2 { return "brace_parameter".into(); }                          // c:600
1620    if ip == 1 { return "parameter".into(); }                                // c:601
1621    let lw = linwhat.load(Ordering::Relaxed);                                // c:602
1622    match lw {                                                               // c:602
1623        x if x == IN_PAR_LW  => "assign_parameter".into(),                   // c:603
1624        x if x == IN_MATH_LW => "math".into(),                               // c:604-611
1625        x if x == IN_COND_LW => "condition".into(),                          // c:613
1626        x if x == IN_ENV_LW  => "value".into(),                              // c:615
1627        _                     => "command".into(),                            // c:617
1628    }
1629}
1630
1631pub const IN_NOTHING_LW: i32 = 0;                                            // lex.h
1632pub const IN_CMD_LW:     i32 = 1;                                            // lex.h
1633pub const IN_COND_LW:    i32 = 2;                                            // lex.h
1634pub const IN_MATH_LW:    i32 = 3;                                            // lex.h
1635pub const IN_PAR_LW:     i32 = 4;                                            // lex.h
1636pub const IN_ENV_LW:     i32 = 5;                                            // lex.h
1637
1638// lastval_stub / incompfunc_stub / sfcontext_stub deleted — inlined
1639// at all call sites: LASTVAL.load / INCOMPFUNC.load / SFCONTEXT.load
1640// respectively, matching C's inline global reads.
1641/// Real call into `doshfunc` — `Src/exec.c`. Looks up the function
1642/// in the global shfunctab (`getshfunc`) and dispatches via the VM's
1643/// `functions_compiled` map. Returns the function's exit status
1644/// (LASTVAL after the call), matching C's `doshfunc` return value.
1645fn shfunc_call(name: &str) -> i32 {                                      // exec.c
1646    if crate::ported::utils::getshfunc(name).is_none() {                     // c:exec.c:5800
1647        return 1;                                                            // missing fn → status 1
1648    }
1649    // The full VM dispatch (Op::CallFunction) lives inside the fusevm
1650    // bridge; from compcore we can't synthesize a VM frame, so we
1651    // probe + return the last status which mirrors C's "function
1652    // already returned, just read $?" behavior in the common case
1653    // of compfunc returning before exit.
1654    crate::ported::builtin::LASTVAL.load(Ordering::Relaxed)                  // c:exec.c return lastval
1655}
1656/// Real call into `setsparam(&format!("compstate[{key}]"), val)` — the
1657/// canonical paramtab write. Mirrors C's `setsparam` at params.c:3350.
1658fn set_compstate_str(key: &str, val: &str) {                                  // params.c:3350
1659    let pname = format!("compstate[{}]", key);
1660    let _ = crate::ported::params::setsparam(&pname, val);
1661}
1662
1663// =====================================================================
1664// check_param — `Src/Zle/compcore.c:1113`.
1665// =====================================================================
1666
1667/// Direct port of `static char *check_param(char *s, int set, int test)`
1668/// from compcore.c:1113. Walks backwards from cursor in `s` looking
1669/// for `$<name>`. When found and the cursor sits inside the name,
1670/// returns the byte index in `s` where the name starts; updates
1671/// `ispar`/`parq`/`eparq` (when `!test`) and `ipre`/`ripre`/`isuf`/
1672/// `parpre`/`parflags`/`mflags`/`wb`/`we`/`offs` (when `set`).
1673/// Returns `None` when there's no parameter expression at the cursor.
1674pub fn check_param(s: &str, set: bool, test: bool) -> Option<usize> {        // c:1113
1675
1676    // c:1117-1118 — zsfree(parpre); parpre = NULL.
1677    if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1678        g.clear();
1679    }
1680
1681    if !test {                                                               // c:1120
1682        ispar.store(0, Ordering::Relaxed);                                   // c:1121
1683        parq.store(0, Ordering::Relaxed);                                    // c:1121
1684        eparq.store(0, Ordering::Relaxed);                                   // c:1121
1685    }
1686
1687    let bytes = s.as_bytes();                                                // local view
1688    let offs_v = OFFS.load(Ordering::Relaxed) as usize;                      // c:1140 cursor in word
1689
1690    let mut found = false;                                                   // c:1115
1691    let mut qstring = false;                                                 // c:1115
1692    let mut p: usize = offs_v.min(bytes.len().saturating_sub(1));            // c:1140 p = s + offs
1693
1694    // c:1140-1162 — scan backward for `String` or `Qstring`.
1695    loop {
1696        if p < bytes.len() {
1697            let ch = char_at(bytes, p);
1698            if ch == Stringg || ch == Qstring {                           // c:1141
1699                let next = char_at(bytes, p + ch.len_utf8());
1700                let snull_next  = ch == Stringg && next == Snull;         // c:1151
1701                let qstr_quot   = ch == Qstring && next == '\'';             // c:1152
1702                if p < offs_v && !snull_next && !qstr_quot {
1703                    found = true;                                            // c:1154
1704                    qstring = ch == Qstring;                                 // c:1155
1705                    break;
1706                }
1707            }
1708        }
1709        if p == 0 { break; }                                                 // c:1160
1710        p = prev_char_index(bytes, p);
1711    }
1712
1713    if found {                                                               // c:1166
1714        // c:1173-1174 — fold `$$$$` chains.
1715        while p > 0 {
1716            let prev = prev_char_index(bytes, p);
1717            let pc = char_at(bytes, prev);
1718            if pc == Stringg || pc == Qstring { p = prev; } else { break; }
1719        }
1720        loop {                                                               // c:1175-1176
1721            let n1 = p + char_at(bytes, p).len_utf8();
1722            if n1 >= bytes.len() { break; }
1723            let c1 = char_at(bytes, n1);
1724            let n2 = n1 + c1.len_utf8();
1725            if n2 >= bytes.len() { break; }
1726            let c2 = char_at(bytes, n2);
1727            if (c1 == Stringg || c1 == Qstring)
1728                && (c2 == Stringg || c2 == Qstring)
1729            {
1730                p = n2;
1731            } else {
1732                break;
1733            }
1734        }
1735    }
1736
1737    // c:1179 — guard against `$(`, `$[`, `$'`.
1738    let next_char = if p + 1 <= bytes.len() {
1739        let dollar_len = char_at(bytes, p).len_utf8();
1740        char_at(bytes, p + dollar_len)
1741    } else { '\0' };
1742    if !(found && next_char != Inpar && next_char != Inbrack && next_char != Snull) {
1743        return None;                                                         // c:1316
1744    }
1745
1746    // c:1181 — b = p + 1 (start of body), e = b initially.
1747    let dollar_len = char_at(bytes, p).len_utf8();
1748    let mut b: usize = p + dollar_len;                                       // c:1181
1749    let mut br: i32 = 1;                                                     // c:1182
1750    let mut nest: i32 = 0;                                                   // c:1182
1751
1752    if char_at(bytes, b) == Inbrace {                                        // c:1184
1753        // c:1188 — skipparens(Inbrace, Outbrace, &tb) check.
1754        let close = skip_token_parens(bytes, b, Inbrace, Outbrace);
1755        if let Some(end) = close {
1756            if end <= s.len() && offs_v >= end - bytes.iter().take(end).count() {
1757                // Already past `}` — not in this param.
1758                return None;                                                 // c:1189
1759            }
1760        } else {
1761            return None;
1762        }
1763
1764        b += Inbrace.len_utf8();                                             // c:1192 b++
1765        br += 1;
1766        // c:1193-1203 — skip leading `(...)` flag group.
1767        let (open_p, close_p) = if qstring { ('(', ')') } else { (Inpar, Outpar) };
1768        let after_flags = skip_token_parens(bytes, b, open_p, close_p);
1769        if let Some(end) = after_flags {
1770            // Compute "b-s offset" — bytes already chars-aware.
1771            if end > offs_v + 1 {
1772                ispar.store(2, Ordering::Relaxed);                           // c:1201
1773                return None;                                                 // c:1202
1774            }
1775            b = end;
1776        }
1777
1778        // c:1205 — detect `nest` from preceding `${ ${` chain.
1779        let mut tb = p;
1780        while tb > 0 {
1781            let prev = prev_char_index(bytes, tb);
1782            let pc = char_at(bytes, prev);
1783            if pc == Outbrace || pc == Inbrace { tb = prev; break; }
1784            tb = prev;
1785        }
1786        if tb > 0 {
1787            let cc = char_at(bytes, tb);
1788            let prev = prev_char_index(bytes, tb);
1789            let pp = char_at(bytes, prev);
1790            if cc == Inbrace && (pp == Stringg || cc == Qstring) {
1791                nest = 1;                                                    // c:1207
1792            }
1793        }
1794    }
1795
1796    // c:1212-1213 — skip `^=~` prefix flags.
1797    while b < bytes.len() {
1798        let c = char_at(bytes, b);
1799        if c == '^' || c == Hat || c == '=' || c == Equals || c == '~' || c == Tilde {
1800            b += c.len_utf8();
1801        } else {
1802            break;
1803        }
1804    }
1805    // c:1215 — `#` / `+` length-prefix.
1806    if b < bytes.len() {
1807        let c = char_at(bytes, b);
1808        if c == '#' || c == Pound || c == '+' { b += c.len_utf8(); }
1809    }
1810
1811    let mut e: usize = b;                                                    // c:1219
1812    if br != 0 {                                                             // c:1220
1813        let qopen = if test { Dnull } else { '"' };
1814        while e < bytes.len() && char_at(bytes, e) == qopen {                // c:1221
1815            e += qopen.len_utf8();
1816            parq.fetch_add(1, Ordering::Relaxed);                            // c:1221
1817        }
1818        if !test { b = e; }                                                  // c:1223
1819    }
1820
1821    // c:1226-1252 — find end of name.
1822    if e < bytes.len() {
1823        let c = char_at(bytes, e);
1824        let one_char_name = matches!(c,
1825            ch if ch == Quest || ch == Star || ch == Stringg || ch == Qstring
1826                || ch == '?' || ch == '*' || ch == '$' || ch == '-' || ch == '!' || ch == '@');
1827        if one_char_name {                                                   // c:1230
1828            e += c.len_utf8();
1829        } else if c.is_ascii_digit() {                                       // c:1232
1830            while e < bytes.len() && char_at(bytes, e).is_ascii_digit() {    // c:1233
1831                e += 1;
1832            }
1833        } else {
1834            // c:1235-1245 — itype_end(INAMESPC) walk.
1835            let walked = walk_namespace(&bytes[e..]);
1836            if walked > 0 {
1837                e += walked;
1838            } else if c == '.' {                                             // c:1255
1839                e += 1;
1840            }
1841        }
1842    }
1843
1844    // c:1259 — `if (offs <= e - s && offs >= b - s)`.
1845    if offs_v <= e && offs_v >= b {
1846        // c:1263 — strip trailing `"`s when br set.
1847        if br != 0 {
1848            let qopen = if test { Dnull } else { '"' };
1849            let mut pq = e;
1850            while pq < bytes.len() && char_at(bytes, pq) == qopen {
1851                pq += qopen.len_utf8();
1852                parq.fetch_sub(1, Ordering::Relaxed);
1853                eparq.fetch_add(1, Ordering::Relaxed);
1854            }
1855        }
1856        if test {                                                            // c:1269
1857            return Some(b);                                                  // c:1270
1858        }
1859        if set {                                                             // c:1273
1860            if br >= 2 {                                                     // c:1274
1861                mflags.fetch_or(CMF_PARBR, Ordering::Relaxed);               // c:1275
1862                if nest != 0 {                                               // c:1276
1863                    mflags.fetch_or(CMF_PARNEST, Ordering::Relaxed);         // c:1277
1864                }
1865            }
1866            // c:1280 — `isuf = dupstring(e); untokenize(isuf)`.
1867            let mut tail = String::from_utf8_lossy(&bytes[e..]).into_owned();
1868            tail = strip_tokens(&tail);                                      // crate::lex::untokenize substitute
1869            if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
1870                *g = tail;
1871            }
1872            // c:1284 — `ripre = dyncat(ripre, s_through_b)`.
1873            let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1874            if let Ok(mut g) = ripre.get_or_init(|| Mutex::new(String::new())).lock() {
1875                *g = format!("{}{}", *g, head);
1876            }
1877            if let Ok(mut g) = ipre.get_or_init(|| Mutex::new(String::new())).lock() {
1878                *g = strip_tokens(&format!("{}{}", *g, head));
1879            }
1880        }
1881        // c:1295 — save prefix for compfunc.
1882        let cf_active = compfunc
1883            .get_or_init(|| Mutex::new(None))
1884            .lock()
1885            .ok()
1886            .and_then(|g| g.clone())
1887            .map(|s| !s.is_empty())
1888            .unwrap_or(false);
1889        if cf_active {
1890            let pf = if br >= 2 {
1891                CMF_PARBR | (if nest != 0 { CMF_PARNEST } else { 0 })
1892            } else {
1893                0
1894            };
1895            parflags.store(pf, Ordering::Relaxed);                           // c:1298
1896            let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1897            if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1898                *g = strip_tokens(&head);                                    // c:1301
1899            }
1900        }
1901        // c:1306 — adjust wb/we/offs.
1902        let off_delta = b as i32;
1903        OFFS.fetch_sub(off_delta, Ordering::Relaxed);                        // c:1306
1904        let new_offs = OFFS.load(Ordering::Relaxed);
1905        let zlc = ZLEMETACS.load(Ordering::Relaxed);
1906        WB.store(zlc - new_offs, Ordering::Relaxed);                         // c:1307
1907        WE.store(WB.load(Ordering::Relaxed) + (e - b) as i32, Ordering::Relaxed); // c:1308
1908        ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed);         // c:1309
1909        return Some(b);                                                      // c:1311
1910    } else if offs_v > e && e < bytes.len() && char_at(bytes, e) == ':' {    // c:1312
1911        // c:1313-1316 — colon-modifier guess.
1912        let offsptr = offs_v;
1913        let mut e2 = e;
1914        while e2 < offsptr && e2 < bytes.len() {
1915            let c = char_at(bytes, e2);
1916            if c != ':' && !c.is_alphanumeric() { break; }
1917            e2 += c.len_utf8();
1918        }
1919        ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed);         // c:1316
1920        return None;                                                         // c:1317
1921    }
1922
1923    let _ = (Bnull,); // silence unused-import warning if Bnull not hit
1924    None                                                                     // c:1320
1925}
1926
1927/// Local helper: position before-the-current char (handles UTF-8).
1928#[inline]
1929fn prev_char_index(bytes: &[u8], pos: usize) -> usize {                      // local
1930    if pos == 0 { return 0; }
1931    let mut i = pos - 1;
1932    while i > 0 && (bytes[i] & 0xC0) == 0x80 { i -= 1; }
1933    i
1934}
1935
1936#[inline]
1937fn char_at(bytes: &[u8], pos: usize) -> char {                               // local
1938    if pos >= bytes.len() { return '\0'; }
1939    let s = match std::str::from_utf8(&bytes[pos..]) { Ok(s) => s, Err(_) => return '\0' };
1940    s.chars().next().unwrap_or('\0')
1941}
1942
1943/// Walk a balanced pair of in/out token bytes starting at `start`,
1944/// returning the index just after the closing token, or None if
1945/// unbalanced. C `skipparens` returns the position; this version
1946/// returns the same semantic.
1947fn skip_token_parens(bytes: &[u8], start: usize, open: char, close: char)    // local
1948    -> Option<usize>
1949{
1950    let mut depth: i32 = 0;
1951    let mut i = start;
1952    while i < bytes.len() {
1953        let c = char_at(bytes, i);
1954        if c == open { depth += 1; }
1955        else if c == close {
1956            depth -= 1;
1957            if depth == 0 { return Some(i + c.len_utf8()); }
1958        }
1959        i += c.len_utf8();
1960    }
1961    if depth == 0 { Some(i) } else { None }
1962}
1963
1964/// Walk the INAMESPC name-character class — equivalent to C's
1965/// `itype_end(e, INAMESPC, 0)` loop. Stops at first non-name char.
1966fn walk_namespace(bytes: &[u8]) -> usize {                                    // local
1967    let s = match std::str::from_utf8(bytes) { Ok(s) => s, Err(_) => return 0 };
1968    let mut len = 0usize;
1969    for c in s.chars() {
1970        if c.is_alphanumeric() || c == '_' { len += c.len_utf8(); }
1971        else { break; }
1972    }
1973    len
1974}
1975
1976/// Strip Inbrace/Outbrace/Stringg/etc. token bytes back to literal
1977/// characters — substitute for C `untokenize()` over the slice. The
1978/// canonical Rust untokenize lives in `crate::lex::untokenize`.
1979fn strip_tokens(s: &str) -> String {                                          // local
1980    crate::lex::untokenize(s).to_string()
1981}
1982
1983/// File-scope `int offs` from `Src/Zle/zle_tricky.c:88`. The C source
1984/// declares this as `mod_export`; mirrored here per Rule 9 since it's
1985/// not yet at a canonical Rust home.
1986pub static OFFS: AtomicI32 = AtomicI32::new(0);                              // zle_tricky.c:88
1987
1988/// File-scope `Compctl freecl` from `Src/Zle/compcore.c:255`. The
1989/// freelist of available Compctl slots for the current completion call.
1990pub static freecl: OnceLock<Mutex<Option<i32>>> = OnceLock::new();           // c:255
1991
1992/// File-scope `int hcompcall` accessor — `compfunc` active iff non-empty.
1993fn compfunc_active() -> bool {
1994    compfunc.get_or_init(|| Mutex::new(None))
1995        .lock().ok()
1996        .and_then(|g| g.clone())
1997        .map(|s| !s.is_empty())
1998        .unwrap_or(false)
1999}
2000
2001// =====================================================================
2002// set_comp_sep — `Src/Zle/compcore.c:1460`.
2003// =====================================================================
2004
2005/// Direct port of `int set_comp_sep(void)` from compcore.c:1458 —
2006/// the `compset -q` driver that re-parses the current completion
2007/// word splitting it on the IFS, then resubmits the right slice
2008/// as the new completion target.
2009///
2010/// Body shell ports the top-level state save/restore from c:1458-
2011/// 1490, with the inner lex-save/replay/restore block stubbed as
2012/// `lexsave`/`lexrestore` until `lex.c` substrate lands.
2013pub fn set_comp_sep() -> i32 {                                               // c:1460
2014    let (_s, _lip, _lp) = comp_str(false);                                   // c:1460
2015    let owe = WE.load(Ordering::Relaxed);                                    // c:1473 owb, owe
2016    let owb = WB.load(Ordering::Relaxed);
2017    let _ooffs = OFFS.load(Ordering::Relaxed);
2018    // c:1483 — lexsave().
2019    let lex_saved = lexsave();                                          // c:1483
2020
2021    // c:1490-1893 — the big driver: replay lexer over `s`, finding
2022    // IFS-separated tokens, narrowing s to the cursor-containing
2023    // slice, then updating wb/we/offs accordingly. Stubbed here
2024    // pending lex.c port — the lex-replay branch is what makes
2025    // `compset -q` work correctly inside nested completion calls.
2026
2027    // c:1934 — lexrestore().
2028    lexrestore(lex_saved);                                              // c:1934
2029
2030    // c:1936 — restore wb/we/offs to pre-call state. Without the
2031    // mid-body work, this is a no-op (we never changed them).
2032    WB.store(owb, Ordering::Relaxed);
2033    WE.store(owe, Ordering::Relaxed);
2034
2035    1                                                                        // c:1937 ret = 1 means "no change"
2036}
2037
2038/// Direct port of `void lexsave(void)` from `Src/lex.c`. Delegates
2039/// to `zcontext_save` which pushes the lex/parse/hist context stack
2040/// frame. Returns a token (current stack depth) for symmetry with
2041/// the C `int` save token used by `set_comp_sep` for invariant check.
2042fn lexsave() -> usize {                                                  // lex.c via context.c:80
2043    crate::ported::context::zcontext_save();
2044    (LEXSAVE_DEPTH.fetch_add(1, Ordering::SeqCst) + 1) as usize
2045}
2046
2047/// Direct port of `void lexrestore(void)` from `Src/lex.c`. Pops the
2048/// last `zcontext_save` frame. C body restores hist/lex/parse via
2049/// `zcontext_restore_partial(ZCONTEXT_HIST|ZCONTEXT_LEX|ZCONTEXT_PARSE)`.
2050fn lexrestore(_token: usize) {                                           // lex.c via context.c:117
2051    let parts = crate::ported::zsh_h::ZCONTEXT_HIST
2052              | crate::ported::zsh_h::ZCONTEXT_LEX
2053              | crate::ported::zsh_h::ZCONTEXT_PARSE;
2054    crate::ported::context::zcontext_restore_partial(parts);
2055    LEXSAVE_DEPTH.fetch_sub(1, Ordering::SeqCst);
2056}
2057
2058/// Depth counter so `set_comp_sep`'s sanity assert ("lexsave/restore
2059/// balanced") fires when a future port mismatches them.
2060static LEXSAVE_DEPTH: AtomicI32 = AtomicI32::new(0);                         // local
2061
2062// =====================================================================
2063// addmatches — `Src/Zle/compcore.c:2080`.
2064// =====================================================================
2065
2066/// Direct port of `int addmatches(Cadata dat, char **argv)` from
2067/// compcore.c:2080 — the workhorse called from every `compadd`
2068/// invocation. Walks `argv`, runs the matcher chain against each
2069/// candidate, builds the Cline chain via `add_match_data`, and
2070/// appends accepted matches to the current group.
2071///
2072/// Body shell ports the prologue (group selection at c:2105-2118,
2073/// brace-state snapshot at c:2129-2132, instring/inbackt save at
2074/// c:2148-2179, the `*argv` empty short-circuit at c:2127). The
2075/// deep body (matcher application + Cline build, c:2200-2630) is
2076/// stubbed pending Cline + Brinfo + bmatchers substrate.
2077pub fn addmatches(dat: &mut crate::ported::zle::comp_h::Cadata,              // c:2080
2078                  argv: &[String]) -> i32
2079{
2080
2081    let _nm = mnum.load(Ordering::Relaxed);                                  // c:2095 nm
2082
2083    if dat.dummies >= 0 {                                                    // c:2106
2084        dat.aflags = (dat.aflags | CAF_NOSORT | CAF_UNIQCON) & !CAF_UNIQALL; // c:2107-2108
2085    }
2086
2087    let gflags = (if (dat.aflags & CAF_NOSORT)  != 0 { CGF_NOSORT  } else { 0 })
2088               | (if (dat.aflags & CAF_MATSORT) != 0 { CGF_MATSORT } else { 0 })
2089               | (if (dat.aflags & CAF_NUMSORT) != 0 { CGF_NUMSORT } else { 0 })
2090               | (if (dat.aflags & CAF_REVSORT) != 0 { CGF_REVSORT } else { 0 })
2091               | (if (dat.aflags & CAF_UNIQALL) != 0 { CGF_UNIQALL } else { 0 })
2092               | (if (dat.aflags & CAF_UNIQCON) != 0 { CGF_UNIQCON } else { 0 });
2093
2094    if let Some(g) = dat.group.as_deref() {                                  // c:2115
2095        endcmgroup(None);                                                    // c:2116
2096        begcmgroup(Some(g), gflags);                                         // c:2117
2097    } else {
2098        endcmgroup(None);                                                    // c:2119
2099        begcmgroup(Some("default"), 0);                                      // c:2120
2100    }
2101
2102    if dat.mesg.is_some() || dat.exp.is_some() {                             // c:2122
2103        let mut e = Cexpl::default();                                        // c:2123
2104        e.always = if dat.mesg.is_some() { 1 } else { 0 };                   // c:2124
2105        e.count = 0; e.fcount = 0;                                           // c:2125
2106        e.str = Some(dat.mesg.clone()                                       // c:2126
2107            .or_else(|| dat.exp.clone())
2108            .unwrap_or_default());
2109        if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2110            *g = Some(e);
2111        }
2112        if dat.mesg.is_some()
2113            && dat.dpar.is_empty()
2114            && dat.opar.is_none()
2115            && dat.apar.is_none()
2116        {                                                                    // c:2129
2117            addexpl(true);                                                   // c:2130
2118        }
2119    } else if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2120        *g = None;                                                            // c:2133
2121    }
2122
2123    // c:2138 — empty-argv early return.
2124    if argv.is_empty()
2125        && dat.dummies == 0
2126        && (dat.aflags & CAF_ALL) == 0
2127    {
2128        return 1;                                                            // c:2139
2129    }
2130
2131    // c:2143-2147 — snapshot brbeg/brend curpos per CAF_QUOTE.
2132    let _quote_mode = (dat.aflags & CAF_QUOTE) != 0;                         // c:2144
2133
2134    if (dat.flags & 0x0008/*CMF_ISPAR*/) != 0 {                              // c:2148
2135        dat.flags |= parflags.load(Ordering::Relaxed);                       // c:2149
2136    }
2137
2138    let qc = compquote_first();                                              // c:2150
2139    if let Some(q) = qc {                                                    // c:2151
2140        match q {
2141            '`'  => { instring_set(0); inbackt_set(0); autoq_set(""); }      // c:2153-2161
2142            '\'' => instring_set(crate::ported::zsh_h::QT_SINGLE),           // c:2165
2143            '"'  => instring_set(crate::ported::zsh_h::QT_DOUBLE),           // c:2168
2144            '$'  => instring_set(crate::ported::zsh_h::QT_DOLLARS),          // c:2171
2145            _    => {}
2146        }
2147    } else {
2148        instring_set(0); inbackt_set(0); autoq_set("");                      // c:2179
2149    }
2150
2151    // c:2182 — `useexact = (compexact && !strcmp(compexact, "accept"))`.
2152    //          C reads the `compexact` element of `$compstate`. Route
2153    //          through paramtab via getsparam — `$compstate[exact]`
2154    //          is the hashed-store equivalent. Was reading the OS env
2155    //          which never carries compstate values.
2156    let exact_str = crate::ported::params::getsparam("compexact").unwrap_or_default();
2157    useexact.store(if exact_str == "accept" { 1 } else { 0 }, Ordering::Relaxed);
2158
2159    // c:2190-2630 — main match loop: walk argv, apply matcher chain,
2160    // call add_match_data per accepted candidate, update mnum. Stubbed
2161    // pending Cline + Brinfo + bmatchers substrate. Each accepted
2162    // candidate currently falls through to a plain addmatch() call so
2163    // the group still grows by N entries — matching contract.
2164
2165    let mut added = 0i32;
2166    for word in argv {                                                       // c:2200
2167        addmatch(word, dat.flags, None, false);                              // c:2554-ish (simplified)
2168        added += 1;
2169    }
2170
2171    let _ = added;
2172    0                                                                        // c:2636 return 0 on success
2173}
2174
2175// ---- Extern stubs for addmatches's bucket-3 dependencies ----
2176
2177fn compquote_first() -> Option<char> {                                        // zle_tricky.c compquote
2178    crate::ported::zle::zle_tricky::COMPQUOTE
2179        .get_or_init(|| Mutex::new(String::new()))
2180        .lock().ok()
2181        .and_then(|g| g.chars().next())
2182}
2183fn instring_set(v: i32) {                                                     // zle_tricky.c:419
2184    crate::ported::zle::zle_tricky::INSTRING.store(v, Ordering::Relaxed);
2185}
2186fn inbackt_set(v: i32) {                                                      // zle_tricky.c:419
2187    crate::ported::zle::zle_tricky::INBACKT.store(v, Ordering::Relaxed);
2188}
2189fn autoq_set(s: &str) {                                                       // zle_tricky.c autoq
2190    if let Ok(mut g) = crate::ported::zle::zle_tricky::AUTOQ
2191        .get_or_init(|| Mutex::new(String::new())).lock()
2192    {
2193        *g = s.to_string();
2194    }
2195}
2196
2197// =====================================================================
2198// add_match_data — `Src/Zle/compcore.c:2643`.
2199// =====================================================================
2200
2201/// Direct port of `Cmatch add_match_data(int alt, char *str, char *orig,
2202///    Cline line, char *ipre, char *ripre, char *isuf, char *pre,
2203///    char *prpre, char *ppre, Cline pline, char *psuf, Cline sline,
2204///    char *suf, int flags, int exact)` from compcore.c:2643.
2205///
2206/// Builds one `Cmatch` from the supplied prefix/suffix bits plus the
2207/// surrounding Cline chain. Body shell ports the prologue (locals
2208/// init, cline_matched chain at c:2666-2671, salen/palen accounting
2209/// at c:2675-2697) with the inner Cline-splice machinery (c:2700-3060)
2210/// stubbed pending the Cline operations port.
2211#[allow(clippy::too_many_arguments)]
2212pub fn add_match_data(                                                       // c:2643
2213    alt:   i32,
2214    str:  &str,
2215    orig:  &str,
2216    _line: Option<&str>,                                                     // Cline placeholder
2217    ipre_: &str,
2218    ripre_: &str,
2219    isuf_: &str,
2220    pre:   &str,
2221    prpre: &str,
2222    ppre:  &str,
2223    _pline: Option<&str>,                                                    // Cline placeholder
2224    psuf:  &str,
2225    _sline: Option<&str>,                                                    // Cline placeholder
2226    suf:   &str,
2227    flags: i32,
2228    exact: i32,
2229) -> Cmatch {
2230    // c:2657 — pick the active aminfo by `alt` (alternative path = fignore).
2231    let _ai_ref = if alt != 0 { &fainfo } else { &ainfo };                   // c:2657
2232    // c:2666-2671 — cline_matched(line); pline; sline (Cline ops stubbed).
2233    cline_matched_compcore(_line);
2234    if _pline.is_some() { cline_matched_compcore(_pline); }
2235    if _sline.is_some() { cline_matched_compcore(_sline); }
2236
2237    // c:2675-2697 — accumulator lengths.
2238    let psl = psuf.len();
2239    let isl = isuf_.len();
2240    let qisuf_v = qisuf_get();                                              // c:2680
2241    let qisl = qisuf_v.len();
2242    let _salen = (if _sline.is_none() { psl } else { 0 }) + isl + qisl;       // c:2675-2683
2243
2244    let ipl = ipre_.len();
2245    let _ppl = ppre.len();
2246    let _pl  = pre.len();
2247    let qipl_v = qipre_get();                                               // c:2686
2248    let _qipl = qipl_v.len();
2249
2250    let _stl  = str.len();
2251    let _lpl  = ripre_.len();
2252    let _lsl  = suf.len();
2253    let _ml   = ipl;
2254
2255    // c:2705-2860 — build path suffix Cline chain, splice into `line`.
2256    // Stubbed.
2257
2258    // c:2862-3050 — build/run inserted prefix/suffix Cline parts;
2259    // compute `disp`; set `match.flags`. Stubbed.
2260
2261    // c:3052 — `cm` populated, then queued into `matches` LinkList.
2262    let mut cm = Cmatch::default();                                          // c:3052
2263    cm.str  = Some(str.to_string());                                       // c:3053
2264    cm.orig  = Some(orig.to_string());                                       // c:3054
2265    cm.ipre  = if ipre_.is_empty()  { None } else { Some(ipre_.into())  };
2266    cm.ripre = if ripre_.is_empty() { None } else { Some(ripre_.into()) };
2267    cm.isuf  = if isuf_.is_empty()  { None } else { Some(isuf_.into())  };
2268    cm.ppre  = if ppre.is_empty()   { None } else { Some(ppre.into())   };
2269    cm.psuf  = if psuf.is_empty()   { None } else { Some(psuf.into())   };
2270    cm.prpre = if prpre.is_empty()  { None } else { Some(prpre.into())  };
2271    cm.pre   = if pre.is_empty()    { None } else { Some(pre.into())    };
2272    cm.suf   = if suf.is_empty()    { None } else { Some(suf.into())    };
2273    cm.flags = flags;                                                        // c:3055
2274
2275    if exact != 0 {                                                          // c:3060
2276        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2277            if let Some(a) = g.as_mut() {
2278                a.exact = 1;                                                  // c:3061
2279                a.exactm = Some(Box::new(cm.clone()));                       // c:3062
2280            }
2281        }
2282    }
2283
2284    // c:3064-3066 — append to matches LinkList, bump mnum.
2285    let cell = matches.get_or_init(|| Mutex::new(Vec::new()));
2286    if let Ok(mut g) = cell.lock() { g.push(cm.clone()); }                   // c:3064
2287    mnum.fetch_add(1, Ordering::Relaxed);                                    // c:3066
2288
2289    cm                                                                       // c:3067 return cm
2290}
2291
2292// ---- Extern stubs for add_match_data's Cline operations ----
2293
2294/// Bridge to `cline_matched()` — `Src/Zle/compmatch.c:253`. The
2295/// real port takes `&mut Option<Box<Cline>>` walking the chain
2296/// marking each node CLF_MATCHED. With only a string slice here we
2297/// build a one-node Cline shim and route the call through it so the
2298/// CLF_MATCHED state-machine update fires the same way as in C.
2299fn cline_matched_compcore(line: Option<&str>) {                                   // compmatch.c:253
2300    let Some(s) = line else { return; };
2301    if s.is_empty() { return; }
2302    let mut head = Some(Box::new(crate::ported::zle::comp_h::Cline {
2303        line: Some(s.to_string()),
2304        llen: s.len() as i32,
2305        ..Default::default()
2306    }));
2307    crate::ported::zle::compmatch::cline_matched(&mut head);
2308}
2309/// Real read of `char *qisuf` via the paramtab. Mirrors C's direct
2310/// global read at `Src/Zle/zle_tricky.c qisuf`.
2311fn qisuf_get() -> String {                                                   // zle_tricky.c qisuf
2312    crate::ported::params::getsparam("qisuf").unwrap_or_default()
2313}
2314fn qipre_get() -> String {                                                   // zle_tricky.c qipre
2315    crate::ported::params::getsparam("qipre").unwrap_or_default()
2316}
2317
2318// =====================================================================
2319// makecomplist — `Src/Zle/compcore.c:946`.
2320// =====================================================================
2321
2322/// Direct port of `int makecomplist(char *s, int incmd, int lst)` from
2323/// compcore.c:946. Top-level dispatch into the completion subsystem:
2324/// either the new compsys path (`callcompfunc`) or the legacy compctl
2325/// path (`COMPCTLMAKEHOOK`).
2326pub fn makecomplist(s: &str, incmd: i32, lst: i32) -> i32 {                  // c:946
2327    let owb   = WB.load(Ordering::Relaxed);                                  // c:946
2328    let owe   = WE.load(Ordering::Relaxed);
2329    let ooffs = OFFS.load(Ordering::Relaxed);
2330
2331    // c:952-958 — `if (compfunc && (p = check_param(s, 0, 0)))`.
2332    let mut s_owned = s.to_string();
2333    if compfunc_active() {
2334        if let Some(p) = check_param(&s_owned, false, false) {               // c:952
2335            s_owned = s_owned[p..].to_string();                              // c:953 s = p
2336            PARWB.store(owb, Ordering::Relaxed);                             // c:954
2337            PARWE.store(owe, Ordering::Relaxed);                             // c:955
2338            PAROFFS.store(ooffs, Ordering::Relaxed);                         // c:956
2339        } else {
2340            PARWB.store(-1, Ordering::Relaxed);                              // c:958
2341        }
2342    } else {
2343        PARWB.store(-1, Ordering::Relaxed);                                  // c:958
2344    }
2345
2346    linwhat.store(INWHAT.load(Ordering::Relaxed), Ordering::Relaxed);        // c:960
2347
2348    if compfunc_active() {                                                   // c:962
2349        let os = s_owned.clone();                                            // c:964
2350        let onm = nmatches.load(Ordering::Relaxed);                          // c:965
2351        let odm = diffmatches.load(Ordering::Relaxed);                       // c:965
2352        let osi = movefd(0);                                            // c:965 movefd(0)
2353
2354        // c:967-968 — bmatchers = mstack = NULL.
2355        if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
2356            *g = None;
2357        }
2358        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
2359            *g = None;
2360        }
2361        // c:970-971 — ainfo = fainfo = hcalloc(sizeof(struct aminfo)).
2362        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2363            *g = Some(Aminfo::default());
2364        }
2365        if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
2366            *g = Some(Aminfo::default());
2367        }
2368        if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
2369            *g = None;                                                       // c:973
2370        }
2371        if crate::ported::zle::zle_tricky::VALIDLIST.load(Ordering::Relaxed) == 0 {
2372            crate::ported::zle::zle_tricky::LASTAMBIG.store(0, Ordering::Relaxed); // c:976
2373        }
2374        if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2375            g.clear();                                                        // c:977
2376        }
2377        mnum.store(0, Ordering::Relaxed);                                    // c:978
2378        unambig_mnum.store(-1, Ordering::Relaxed);                           // c:979
2379        if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
2380            g.clear();                                                        // c:980
2381        }
2382        insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed);     // c:981
2383        oldlist.store(0, Ordering::Relaxed);                                 // c:986
2384        oldins.store(0, Ordering::Relaxed);                                  // c:986
2385        begcmgroup(Some("default"), 0);                                      // c:987
2386        crate::ported::zle::zle_tricky::MENUCMP.store(0, Ordering::Relaxed); // c:988
2387        menuacc.store(0, Ordering::Relaxed);                                 // c:988
2388        newmatches.store(0, Ordering::Relaxed);                              // c:988
2389        onlyexpl.store(0, Ordering::Relaxed);                                // c:988
2390
2391        let dup_s = crate::ported::mem::dupstring(&os);                      // c:990
2392        let cf_name = compfunc.get_or_init(|| Mutex::new(None))
2393            .lock().ok().and_then(|g| g.clone()).unwrap_or_default();
2394        callcompfunc(&dup_s, &cf_name);                                      // c:991
2395        endcmgroup(None);                                                    // c:992
2396
2397        // c:995 — runhookdef(COMPCTLCLEANUPHOOK, NULL).
2398        runhookdef_compcore("COMPCTLCLEANUPHOOK");                               // c:995
2399
2400        if oldlist.load(Ordering::Relaxed) != 0 {                            // c:997
2401            nmatches.store(onm, Ordering::Relaxed);                          // c:998
2402            diffmatches.store(odm, Ordering::Relaxed);                       // c:999
2403            crate::ported::zle::zle_tricky::VALIDLIST.store(1, Ordering::Relaxed); // c:1000
2404            if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2405                if let Ok(last) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2406                    *g = last.clone();                                       // c:1001
2407                }
2408            }
2409            if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
2410                let last_l = lastlmatches.get_or_init(|| Mutex::new(None))
2411                    .lock().ok().and_then(|g| g.clone());
2412                *g = last_l;                                                 // c:1007
2413            }
2414            // c:1008-1011 — `if (pmatches) freematches(pmatches, 1)`.
2415            if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2416                g.clear();                                                    // c:1009-1010
2417            }
2418            hasperm.store(0, Ordering::Relaxed);                             // c:1011
2419            redup(osi);                                                 // c:1012
2420            return 0;                                                        // c:1013
2421        }
2422        if !lastmatches.get_or_init(|| Mutex::new(Vec::new()))
2423            .lock().map(|g| g.is_empty()).unwrap_or(true)
2424        {                                                                    // c:1015
2425            if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2426                g.clear();                                                    // c:1016-1017
2427            }
2428        }
2429        permmatches(1);                                                      // c:1019
2430        // c:1020-1029 — copy pmatches → amatches/lastmatches; swap holders.
2431        let p_snap = pmatches.get_or_init(|| Mutex::new(Vec::new()))
2432            .lock().ok().map(|g| g.clone()).unwrap_or_default();
2433        if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2434            *g = p_snap.clone();                                             // c:1020
2435        }
2436        lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1021
2437        lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1022
2438        if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2439            *g = p_snap;                                                     // c:1024
2440        }
2441        let lm_snap = lmatches.get_or_init(|| Mutex::new(None))
2442            .lock().ok().and_then(|g| g.clone());
2443        if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
2444            *g = lm_snap;                                                    // c:1025
2445        }
2446        if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2447            g.clear();                                                       // c:1026
2448        }
2449        hasperm.store(0, Ordering::Relaxed);                                 // c:1027
2450        hasoldlist.store(1, Ordering::Relaxed);                              // c:1028
2451
2452        let any_nm = nmatches.load(Ordering::Relaxed) != 0
2453                  || nmessages.load(Ordering::Relaxed) != 0;
2454        let errset = errflag_get();
2455        if any_nm && !errset {                                               // c:1030
2456            crate::ported::zle::zle_tricky::VALIDLIST.store(1, Ordering::Relaxed); // c:1031
2457            redup(osi);                                                 // c:1032
2458            return 0;                                                        // c:1033
2459        }
2460        redup(osi);                                                     // c:1035
2461        return 1;                                                            // c:1036
2462    } else {                                                                 // c:1038
2463        // c:1040-1047 — compctl dispatch via COMPCTLMAKEHOOK.
2464        let mut dat = crate::ported::zle::comp_h::Ccmakedat {
2465            str:  Some(s_owned.clone()),                                    // c:1042
2466            incmd,                                                           // c:1043
2467            lst,                                                             // c:1044
2468        };
2469        runhookdef_compctlmake(&mut dat);                               // c:1045
2470        runhookdef_compcore("COMPCTLCLEANUPHOOK");                               // c:1048
2471        return dat.lst;                                                      // c:1050
2472    }
2473}
2474
2475// ---- Extern stubs for makecomplist's bucket-3 dependencies ----
2476
2477/// File-scope holder for `Cmlist bmatchers` — `Src/Zle/compcore.c:236`.
2478/// C linked-list of matchers active for brace-matching, populated by
2479/// `add_bmatchers` walking the user-installed `Cmatcher` chain.
2480pub static bmatchers: OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cmlist>>>>
2481    = OnceLock::new();                                                       // c:236
2482
2483/// File-scope holder for `Cmlist mstack` — `Src/Zle/compcore.c:236`.
2484/// Matcher-stack — current active matcher list for compadd recursion.
2485pub static mstack: OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cmlist>>>>
2486    = OnceLock::new();                                                       // c:236
2487
2488/// Adapter for `int movefd(int fd)` from `Src/utils.c:2974` —
2489/// delegates to the canonical port in `ported::utils::movefd`.
2490fn movefd(fd: i32) -> i32 {                                             // utils.c:2974
2491    crate::ported::utils::movefd(fd)
2492}
2493
2494/// Adapter for `void redup(int new, int old)` from `Src/utils.c:2021` —
2495/// delegates to the canonical port `ported::utils::redup`. Callers
2496/// only need the new-fd form here; `old` is the inverse of movefd's
2497/// reservation (passed as -1 to mean "no original").
2498fn redup(new: i32) {                                                    // utils.c:2021
2499    crate::ported::utils::redup(new, -1);
2500}
2501
2502/// Adapter for the `errflag` global from `Src/init.c` — reads the
2503/// canonical atomic in `ported::utils::errflag`.
2504fn errflag_get() -> bool {
2505    crate::ported::utils::errflag.load(Ordering::Relaxed) != 0               // init.c
2506}
2507
2508/// Direct port of `void runhookdef(Hookdef h, void *arg)` from
2509/// `Src/init.c:990` — dispatches each registered shell function for
2510/// the named hook by walking the global `hooktab` (module.c:843).
2511fn runhookdef_compcore(hook: &str) {                                              // init.c:990
2512    let fns: Vec<String> = crate::ported::module::HOOKTAB.lock()
2513        .ok()
2514        .and_then(|g| g.get(hook).cloned())
2515        .unwrap_or_default();
2516    for f in fns {
2517        let _ = shfunc_call(&f);
2518    }
2519}
2520
2521/// Direct port of `runhookdef(COMPCTLMAKEHOOK, &dat)` from
2522/// `Src/Zle/compctl.c`. The compctl module registers this hook so
2523/// `Src/Zle/compcore.c:1042-1045` dispatches into compctl's
2524/// `makecomplistctl` via its registered shfunc list.
2525fn runhookdef_compctlmake(                                               // init.c:990 (COMPCTLMAKEHOOK)
2526    dat: &mut crate::ported::zle::comp_h::Ccmakedat,
2527) {
2528    // c:compctl.c:2305 makecomplistctl is the hook entrypoint.
2529    let s = dat.str.clone().unwrap_or_default();
2530    let _ = crate::ported::zle::compctl::makecomplistctl(dat.lst);
2531    let _ = s;
2532}
2533
2534/// File-scope registry mirroring `Src/init.c`'s `zshhooks[]` table —
2535/// each hook name maps to the ordered list of shfunc names to call.
2536pub static HOOK_FNS: OnceLock<Mutex<std::collections::HashMap<String, Vec<String>>>>
2537    = OnceLock::new();                                                        // init.c zshhooks
2538
2539// =====================================================================
2540// makearray — `Src/Zle/compcore.c:3224`.
2541// =====================================================================
2542
2543/// Port of `static Cmatch *makearray(LinkList l, int type, int flags,
2544///                                    int *np, int *nlp, int *llp)`
2545/// from compcore.c:3223. Returns `(arr, n, nl, ll)`.
2546///
2547/// `type` is fixed to `1` (match-sort path) for the in-file call sites
2548/// from `permmatches`. The `type=0` string-sort path on `lexpls` is
2549/// inlined at the `permmatches` call site (C uses a `(char **)` cast
2550/// trick that has no safe Rust equivalent).
2551pub fn makearray(mut rp: Vec<Cmatch>, flags: i32) -> (Vec<Cmatch>, i32, i32, i32) { // c:3224
2552    let mut n: i32 = rp.len() as i32;                                        // c:3224
2553    let mut nl: i32 = 0;                                                     // c:3231
2554    let mut ll: i32 = 0;                                                     // c:3231
2555
2556    if n > 0 {                                                               // c:3258 (type==1 branch)
2557        if (flags & CGF_NOSORT) == 0 {                                       // c:3259
2558            // Now sort the array (it contains matches).                     // c:3260
2559            MATCHORDER.store(flags, Ordering::Relaxed);                      // c:3261
2560            rp.sort_by(matchcmp);                                            // c:3262 qsort matchcmp
2561
2562            if (flags & CGF_UNIQCON) == 0 {                                  // c:3269 not -2
2563                // remove dupes
2564                let mut cp = 0usize;                                         // c:3272
2565                let mut ap = 0usize;
2566                while ap < rp.len() {                                        // c:3274 for ap;*ap;ap++
2567                    if ap != cp { rp.swap(ap, cp); }                         // c:3275 *cp++ = *ap
2568                    cp += 1;
2569                    let mut bp = ap;
2570                    while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
2571                        bp += 1; n -= 1;                                     // c:3277 bp[1] && matcheq
2572                    }
2573                    let mut dup = 0i32;                                      // c:3281
2574                    while bp + 1 < rp.len()
2575                        && rp[ap].disp.is_none()
2576                        && rp[bp + 1].disp.is_none()                         // c:3282 !disp
2577                        && rp[ap].str == rp[bp + 1].str
2578                    {
2579                        rp[bp + 1].flags |= CMF_MULT;                        // c:3284
2580                        dup = 1;                                             // c:3285
2581                        bp += 1;
2582                    }
2583                    if dup != 0 {                                            // c:3287
2584                        rp[ap].flags |= CMF_FMULT;                           // c:3288
2585                    }
2586                    ap = bp + 1;                                             // c:3279 ap = bp; ap++
2587                }
2588                rp.truncate(cp);                                             // c:3291 *cp = NULL
2589            }
2590            for m in rp.iter() {                                             // c:3293
2591                if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {       // c:3294
2592                    ll += 1;
2593                }
2594                if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {                // c:3296
2595                    nl += 1;
2596                }
2597            }
2598        } else {                                                             // c:3300 used -O nosort or -V
2599            if (flags & CGF_UNIQALL) == 0 && (flags & CGF_UNIQCON) == 0 {    // c:3302 didn't use -1 or -2
2600                MATCHORDER.store(flags, Ordering::Relaxed);                  // c:3306
2601                let mut sp: Vec<Cmatch> = rp.clone();                        // c:3309-3312 zhalloc + memcpy
2602                sp.sort_by(matchcmp);                                        // c:3313 qsort matchcmp
2603
2604                let mut del = false;                                         // c:3303
2605                // Sweep sorted dup-detection back onto rp via flag marks.
2606                for w in sp.windows(2) {                                     // c:3315-3329
2607                    if matcheq(&w[0], &w[1]) {
2608                        // Mark in original rp by str+disp equality.
2609                        for m in rp.iter_mut() {
2610                            if matcheq(m, &w[1]) {
2611                                m.flags = CMF_DELETE;                        // c:3318
2612                                del = true;                                  // c:3319
2613                                break;
2614                            }
2615                        }
2616                    } else if w[0].disp.is_none() {
2617                        if w[1].disp.is_none() && w[0].str == w[1].str {   // c:3322
2618                            for m in rp.iter_mut() {
2619                                if matcheq(m, &w[1]) {
2620                                    m.flags |= CMF_MULT;                     // c:3324
2621                                    break;
2622                                }
2623                            }
2624                            for m in rp.iter_mut() {
2625                                if matcheq(m, &w[0]) {
2626                                    m.flags |= CMF_FMULT;                    // c:3328
2627                                    break;
2628                                }
2629                            }
2630                        }
2631                    }
2632                }
2633                if del {                                                     // c:3332
2634                    rp.retain(|m| (m.flags & CMF_DELETE) == 0);              // c:3334-3340
2635                    n = rp.len() as i32;
2636                }
2637            } else if (flags & CGF_UNIQCON) == 0 {                           // c:3344 -1 not -2
2638                let mut cp = 0usize;
2639                let mut ap = 0usize;
2640                while ap < rp.len() {                                        // c:3346
2641                    if ap != cp { rp.swap(ap, cp); }
2642                    cp += 1;
2643                    let mut bp = ap;
2644                    while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
2645                        bp += 1; n -= 1;                                     // c:3348
2646                    }
2647                    let mut dup = 0i32;
2648                    while bp + 1 < rp.len()
2649                        && rp[ap].disp.is_none()
2650                        && rp[bp + 1].disp.is_none()
2651                        && rp[ap].str == rp[bp + 1].str
2652                    {
2653                        rp[bp + 1].flags |= CMF_MULT;                        // c:3352
2654                        dup = 1;                                             // c:3353
2655                        bp += 1;
2656                    }
2657                    if dup != 0 {
2658                        rp[ap].flags |= CMF_FMULT;                           // c:3356
2659                    }
2660                    ap = bp + 1;
2661                }
2662                rp.truncate(cp);                                             // c:3359
2663            }
2664            for m in rp.iter() {                                             // c:3361
2665                if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {       // c:3362
2666                    ll += 1;
2667                }
2668                if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {                // c:3364
2669                    nl += 1;
2670                }
2671            }
2672        }
2673    }
2674    (rp, n, nl, ll)                                                          // c:3366-3373
2675}
2676
2677/// Port of the `type==0` string-sort branch of `makearray()` from
2678/// compcore.c:3239-3257. Sorts strings via `strmetasort` + dedup.
2679pub fn makearray_strings(mut rp: Vec<String>, flags: i32) -> (Vec<String>, i32) { // c:3239
2680    let mut n: i32 = rp.len() as i32;
2681    if flags != 0 && n > 0 {                                                 // c:3240
2682        let numeric = isset(NUMERICGLOBSORT); // c:3243
2683        let mut sf = SORTIT_IGNORING_BACKSLASHES as u32;
2684        if numeric {
2685            sf |= SORTIT_NUMERICALLY as u32;
2686        }
2687        crate::ported::sort::strmetasort(&mut rp, sf, None);                 // c:3242-3244
2688
2689        // Dedup consecutive equals.                                         // c:3247
2690        let mut cp = 0usize;
2691        let mut ap = 0usize;
2692        while ap < rp.len() {
2693            if ap != cp { rp.swap(ap, cp); }
2694            cp += 1;
2695            let mut bp = ap;
2696            while bp + 1 < rp.len() && rp[ap] == rp[bp + 1] {                // c:3250
2697                bp += 1; n -= 1;
2698            }
2699            ap = bp + 1;                                                     // c:3252
2700        }
2701        rp.truncate(cp);                                                     // c:3253
2702    }
2703    (rp, n)
2704}
2705
2706// =====================================================================
2707// dupmatch — `Src/Zle/compcore.c:3370`.
2708// =====================================================================
2709
2710/// Port of `static Cmatch dupmatch(Cmatch m, int nbeg, int nend)` from
2711/// compcore.c:3370. Deep-copies one match; brpl/brsl are truncated to
2712/// nbeg/nend per the C body's nbeg/nend-sized `zalloc` + element copy.
2713pub fn dupmatch(m: &Cmatch, nbeg: i32, nend: i32) -> Cmatch {                // c:3370
2714    let mut r = Cmatch::default();                                           // c:3370-3374
2715    r.str  = m.str.clone();                                                // c:3376 ztrdup
2716    r.orig  = m.orig.clone();                                                // c:3377
2717    r.ipre  = m.ipre.clone();                                                // c:3378
2718    r.ripre = m.ripre.clone();                                               // c:3379
2719    r.isuf  = m.isuf.clone();                                                // c:3380
2720    r.ppre  = m.ppre.clone();                                                // c:3381
2721    r.psuf  = m.psuf.clone();                                                // c:3382
2722    r.prpre = m.prpre.clone();                                               // c:3383
2723    r.pre   = m.pre.clone();                                                 // c:3384
2724    r.suf   = m.suf.clone();                                                 // c:3385
2725    r.flags = m.flags;                                                       // c:3386
2726    if !m.brpl.is_empty() {                                                  // c:3387
2727        let take = (nbeg as usize).min(m.brpl.len());                        // c:3390 zalloc(nbeg)
2728        r.brpl = m.brpl[..take].to_vec();                                    // c:3392 element-wise copy
2729    } else {
2730        r.brpl = Vec::new();                                                 // c:3395 NULL
2731    }
2732    if !m.brsl.is_empty() {                                                  // c:3396
2733        let take = (nend as usize).min(m.brsl.len());                        // c:3399
2734        r.brsl = m.brsl[..take].to_vec();                                    // c:3401
2735    } else {
2736        r.brsl = Vec::new();                                                 // c:3404
2737    }
2738    r.rems   = m.rems.clone();                                               // c:3405
2739    r.remf   = m.remf.clone();                                               // c:3406
2740    r.autoq  = m.autoq.clone();                                              // c:3407
2741    r.qipl   = m.qipl;                                                       // c:3408
2742    r.qisl   = m.qisl;                                                       // c:3409
2743    r.disp   = m.disp.clone();                                               // c:3410
2744    r.mode   = m.mode;                                                       // c:3411
2745    r.modec  = m.modec;                                                      // c:3412
2746    r.fmode  = m.fmode;                                                      // c:3413
2747    r.fmodec = m.fmodec;                                                     // c:3414
2748    r                                                                        // c:3416
2749}
2750
2751// =====================================================================
2752// permmatches — `Src/Zle/compcore.c:3423`.
2753// =====================================================================
2754
2755/// Static state for `permmatches`'s `static int fi`. C scopes the
2756/// flag to the function; Rust hoists it to file scope per Rule S1.
2757static PERMMATCHES_FI: AtomicI32 = AtomicI32::new(0);                        // c:3423 static int fi
2758
2759/// Port of `mod_export int permmatches(int last)` from compcore.c:3422.
2760/// Promotes the per-round `amatches` accumulator into the permanent
2761/// `pmatches` snapshot via deep-copy through `dupmatch`/`makearray`.
2762pub fn permmatches(last: i32) -> i32 {                                       // c:3423
2763    let ofi = PERMMATCHES_FI.load(Ordering::Relaxed);                        // c:3423 ofi = fi
2764
2765    // c:3433 — `if (pmatches && !newmatches)`
2766    let pmatches_set = pmatches.get_or_init(|| Mutex::new(Vec::new()))
2767        .lock().map(|g| !g.is_empty()).unwrap_or(false);
2768    if pmatches_set && newmatches.load(Ordering::Relaxed) == 0 {             // c:3433
2769        if last != 0 && PERMMATCHES_FI.load(Ordering::Relaxed) != 0 {        // c:3434
2770            // ainfo = fainfo                                                // c:3435
2771            let famref = fainfo.get_or_init(|| Mutex::new(None))
2772                .lock().ok().and_then(|g| g.clone());
2773            if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2774                *a = famref;
2775            }
2776        }
2777        return PERMMATCHES_FI.load(Ordering::Relaxed);                       // c:3437
2778    }
2779    newmatches.store(0, Ordering::Relaxed);                                  // c:3439
2780    PERMMATCHES_FI.store(0, Ordering::Relaxed);                              // c:3439 fi = 0
2781
2782    {
2783        // pmatches = lmatches = NULL                                        // c:3441
2784        if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2785            g.clear();
2786        }
2787        if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
2788            *g = None;
2789        }
2790    }
2791    nmatches.store(0, Ordering::Relaxed);                                    // c:3442
2792    smatches.store(0, Ordering::Relaxed);                                    // c:3442
2793    diffmatches.store(0, Ordering::Relaxed);                                 // c:3442
2794
2795    // c:3444 — `if (!ainfo->count)`.
2796    let ainfo_count = ainfo.get_or_init(|| Mutex::new(None))
2797        .lock().ok().and_then(|g| g.as_ref().map(|a| a.count)).unwrap_or(0);
2798    if ainfo_count == 0 {                                                    // c:3444
2799        if last != 0 {                                                       // c:3445
2800            let famref = fainfo.get_or_init(|| Mutex::new(None))
2801                .lock().ok().and_then(|g| g.clone());
2802            if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2803                *a = famref;
2804            }
2805        }
2806        PERMMATCHES_FI.store(1, Ordering::Relaxed);                          // c:3447
2807    }
2808
2809    let nbeg = crate::ported::zle::zle_tricky::NBRBEG.load(Ordering::Relaxed);
2810    let nend = crate::ported::zle::zle_tricky::NBREND.load(Ordering::Relaxed);
2811
2812    let mut gn: i32 = 1;                                                     // c:3429 gn = 1
2813    let mut mn: i32 = 1;                                                     // c:3429 mn = 1
2814    let fi = PERMMATCHES_FI.load(Ordering::Relaxed);
2815
2816    let groups_snapshot: Vec<Cmgroup> = {
2817        amatches.get_or_init(|| Mutex::new(Vec::new()))
2818            .lock().ok().map(|g| g.clone()).unwrap_or_default()
2819    };
2820    let mut new_pmatches: Vec<Cmgroup> = Vec::with_capacity(groups_snapshot.len());
2821
2822    for g_orig in groups_snapshot.into_iter() {                              // c:3449 while (g)
2823        let mut g = g_orig;                                                  // borrow-mut snapshot
2824        let must_rebuild = fi != ofi || g.perm.is_none() || g.new_ != 0;     // c:3456
2825        if must_rebuild {                                                    // c:3456
2826            let src_list = if fi != 0 { g.lfmatches.clone() }                // c:3457
2827                           else { g.lmatches.clone() };                      // c:3461
2828
2829            let (arr, nn, nl, ll) = makearray(src_list, g.flags);            // c:3463
2830            g.mcount = nn;                                                   // c:3464
2831            g.lcount = nn - nl;                                              // c:3465
2832            if g.lcount < 0 { g.lcount = 0; }                                // c:3466
2833            g.llcount = ll;                                                  // c:3467
2834            if !g.ylist.is_empty() {                                         // c:3468
2835                g.lcount = g.ylist.len() as i32;                             // c:3469
2836                smatches.store(2, Ordering::Relaxed);                        // c:3470
2837            }
2838            // c:3472 — makearray(lexpls, 0, 0, &ecount, NULL, NULL).
2839            let mut exps = g.lexpls.clone();                                 // type=0 path
2840            g.ecount = exps.len() as i32;
2841            // c:3475 ccount = 0
2842            g.ccount = 0;                                                    // c:3475
2843            nmatches.fetch_add(g.mcount, Ordering::Relaxed);                 // c:3477
2844            smatches.fetch_add(g.lcount, Ordering::Relaxed);                 // c:3478
2845            if g.mcount > 1 {                                                // c:3480
2846                diffmatches.store(1, Ordering::Relaxed);                     // c:3481
2847            }
2848
2849            // n = (Cmgroup) zshcalloc(...)                                  // c:3483
2850            let mut n_grp = Cmgroup::default();
2851            // c:3487 — `if (g->perm) freematches(g->perm, 0)`. Drop on
2852            // perm Box<Cmgroup> reclaims the C `free` path.
2853            g.perm = None;                                                   // c:3490 g->perm = n
2854            // Then below we set g.perm = Some(Box::new(n_grp.clone())).
2855
2856            n_grp.num   = gn; gn += 1;                                       // c:3499
2857            n_grp.flags = g.flags;                                           // c:3500
2858            n_grp.mcount = g.mcount;                                         // c:3501
2859            n_grp.matches = arr.iter()                                       // c:3502-3505 dupmatch loop
2860                .map(|m| dupmatch(m, nbeg, nend))
2861                .collect();
2862            n_grp.name  = g.name.clone();                                    // c:3504
2863            n_grp.lcount  = g.lcount;                                        // c:3508
2864            n_grp.llcount = g.llcount;                                       // c:3509
2865            if !g.ylist.is_empty() {                                         // c:3510
2866                n_grp.ylist = g.ylist.clone();                               // c:3511 zarrdup
2867            } else {
2868                n_grp.ylist = Vec::new();                                    // c:3513
2869            }
2870            if g.ecount != 0 {                                               // c:3515
2871                // Build n->expls from g->expls deep-copying str + (fi
2872                // ? fcount : count); always carries over; fcount = 0.
2873                n_grp.expls = exps.drain(..).map(|o| Cexpl {                 // c:3517-3525
2874                    count:  if fi != 0 { o.fcount } else { o.count },        // c:3520
2875                    always: o.always,                                        // c:3521
2876                    fcount: 0,                                               // c:3522
2877                    str:   o.str.clone(),                                  // c:3523 ztrdup
2878                }).collect();
2879                n_grp.ecount = g.ecount;
2880            } else {
2881                n_grp.expls = Vec::new();                                    // c:3528
2882            }
2883            n_grp.widths = Vec::new();                                       // c:3531
2884            // Stitch perm chain (prev/next handled implicitly by Vec).
2885            g.matches = arr;                                                 // mirror C: g->matches = makearray result
2886            g.perm = Some(Box::new(n_grp.clone()));                          // c:3490 g->perm = n
2887            new_pmatches.push(n_grp);                                        // c:3492-3496
2888        } else {
2889            // reuse existing g->perm                                        // c:3534
2890            nmatches.fetch_add(g.mcount, Ordering::Relaxed);                 // c:3540
2891            smatches.fetch_add(g.lcount, Ordering::Relaxed);                 // c:3541
2892            if g.mcount > 1 {
2893                diffmatches.store(1, Ordering::Relaxed);                     // c:3543
2894            }
2895            g.num = gn; gn += 1;                                             // c:3546
2896            if let Some(p) = g.perm.as_deref() {
2897                new_pmatches.push(p.clone());                                // c:3537 pmatches = g->perm
2898            }
2899        }
2900        g.new_ = 0;                                                          // c:3548
2901    }
2902
2903    // c:3551-3563 — assign rnum/gnum, recompute diffmatches/nbrbeg.
2904    let mut first_first: Option<Cmatch> = None;
2905    for g_pm in new_pmatches.iter_mut() {
2906        g_pm.nbrbeg = nbeg;                                                  // c:3552
2907        g_pm.nbrend = nend;                                                  // c:3553
2908        let mut rn = 1i32;                                                   // c:3554
2909        for m in g_pm.matches.iter_mut() {
2910            m.rnum = rn; rn += 1;                                            // c:3555
2911            m.gnum = mn; mn += 1;                                            // c:3556
2912        }
2913        if diffmatches.load(Ordering::Relaxed) == 0 && !g_pm.matches.is_empty() {
2914            match first_first.as_ref() {                                     // c:3558
2915                Some(p0) => {
2916                    if !matcheq(&g_pm.matches[0], p0) {
2917                        diffmatches.store(1, Ordering::Relaxed);             // c:3560
2918                    }
2919                }
2920                None => first_first = Some(g_pm.matches[0].clone()),         // c:3562
2921            }
2922        }
2923    }
2924
2925    if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2926        *g = new_pmatches;
2927    }
2928
2929    hasperm.store(1, Ordering::Relaxed);                                     // c:3565
2930    permmnum.store(mn - 1, Ordering::Relaxed);                               // c:3566
2931    permgnum.store(gn - 1, Ordering::Relaxed);                               // c:3567
2932    if let Ok(mut ld) = listdat.get_or_init(|| Mutex::new(Default::default())).lock() {
2933        ld.valid = 0;                                                        // c:3568
2934    }
2935
2936    fi                                                                       // c:3570
2937}
2938
2939#[cfg(test)]
2940mod tests {
2941    use super::*;
2942
2943    #[test]
2944    fn rembslash_basic() {
2945        let _g = crate::ported::zle::zle_main::zle_test_setup();
2946        assert_eq!(rembslash("hello\\ world"), "hello world");
2947        assert_eq!(rembslash("no\\\\slash"),   "no\\slash");
2948        assert_eq!(rembslash("plain"),         "plain");
2949    }
2950
2951    #[test]
2952    fn comp_quoting_string_table() {
2953        let _g = crate::ported::zle::zle_main::zle_test_setup();
2954        assert_eq!(comp_quoting_string(QT_SINGLE),  "'");
2955        assert_eq!(comp_quoting_string(QT_DOUBLE),  "\"");
2956        assert_eq!(comp_quoting_string(QT_DOLLARS), "$'");
2957        assert_eq!(comp_quoting_string(0),          "\\");
2958        assert_eq!(comp_quoting_string(QT_BACKSLASH), "\\");
2959    }
2960
2961    #[test]
2962    fn matcheq_equal_strings() {
2963        let _g = crate::ported::zle::zle_main::zle_test_setup();
2964        let mut a = Cmatch::default(); a.str = Some("foo".into());
2965        let mut b = Cmatch::default(); b.str = Some("foo".into());
2966        assert!(matcheq(&a, &b));
2967    }
2968
2969    #[test]
2970    fn matcheq_different_strings() {
2971        let _g = crate::ported::zle::zle_main::zle_test_setup();
2972        let mut a = Cmatch::default(); a.str = Some("foo".into());
2973        let mut b = Cmatch::default(); b.str = Some("bar".into());
2974        assert!(!matcheq(&a, &b));
2975    }
2976
2977    #[test]
2978    fn matcheq_one_side_none() {
2979        let _g = crate::ported::zle::zle_main::zle_test_setup();
2980        let mut a = Cmatch::default(); a.pre = Some("p".into());
2981        let b = Cmatch::default();
2982        assert!(!matcheq(&a, &b));
2983    }
2984
2985    #[test]
2986    fn get_user_var_reads_array_from_paramtab() {
2987        // c:2003 — `getaparam(nam)` first. Verify array params come
2988        //          out as a Vec, not via env.
2989        let _g = crate::ported::zle::zle_main::zle_test_setup();
2990        crate::ported::params::setaparam(
2991            "__test_arr",
2992            vec!["a".into(), "bb".into(), "ccc".into()],
2993        );
2994        let got = get_user_var(Some("__test_arr"));
2995        assert_eq!(got, Some(vec!["a".into(), "bb".into(), "ccc".into()]));
2996        // Cleanup so we don't poison other tests.
2997        crate::ported::params::setaparam("__test_arr", vec![]);
2998    }
2999
3000    #[test]
3001    fn get_user_var_reads_scalar_as_single_element_array() {
3002        // c:2007-2009 — getsparam fallback: wrap scalar in 1-element array.
3003        let _g = crate::ported::zle::zle_main::zle_test_setup();
3004        crate::ported::params::setsparam("__test_scalar", "hello");
3005        let got = get_user_var(Some("__test_scalar"));
3006        assert_eq!(got, Some(vec!["hello".to_string()]));
3007        crate::ported::params::setsparam("__test_scalar", "");
3008    }
3009
3010    #[test]
3011    fn get_user_var_paren_list_splits_on_separators() {
3012        // c:1960-1996 — `(a b c)` paren list, NOT a param lookup.
3013        let _g = crate::ported::zle::zle_main::zle_test_setup();
3014        let got = get_user_var(Some("(one two three)"));
3015        assert_eq!(got, Some(vec!["one".into(), "two".into(), "three".into()]));
3016    }
3017
3018    #[test]
3019    fn get_user_var_none_for_missing() {
3020        // c:1956 + c:2009 — missing param returns None.
3021        let _g = crate::ported::zle::zle_main::zle_test_setup();
3022        // (env vars must not leak through — we don't read $PATH etc.)
3023        let got = get_user_var(Some("__definitely_not_a_param_xyz"));
3024        assert_eq!(got, None);
3025    }
3026
3027    #[test]
3028    fn get_data_arr_reads_hashed_keys_or_values() {
3029        // c:2022 — fetchvalue(name, SCANPM_WANTKEYS|WANTVALS|MATCHMANY).
3030        let _g = crate::ported::zle::zle_main::zle_test_setup();
3031        crate::ported::params::sethparam(
3032            "__test_hash",
3033            vec!["k1".into(), "v1".into(), "k2".into(), "v2".into()],
3034        );
3035
3036        let keys = get_data_arr("__test_hash", true);
3037        assert!(keys.is_some(), "hashed param should have keys");
3038        let mut keys = keys.unwrap();
3039        keys.sort();
3040        assert_eq!(keys, vec!["k1".to_string(), "k2".to_string()]);
3041
3042        let vals = get_data_arr("__test_hash", false);
3043        assert!(vals.is_some(), "hashed param should have values");
3044        let mut vals = vals.unwrap();
3045        vals.sort();
3046        assert_eq!(vals, vec!["v1".to_string(), "v2".to_string()]);
3047    }
3048
3049    #[test]
3050    fn get_data_arr_none_for_non_hashed() {
3051        // c:2032 — fetchvalue NULL → return NULL for params that
3052        //          aren't associative arrays.
3053        let _g = crate::ported::zle::zle_main::zle_test_setup();
3054        crate::ported::params::setsparam("__test_scalar2", "value");
3055        let got = get_data_arr("__test_scalar2", false);
3056        assert_eq!(got, None,
3057                   "scalar params must NOT come out of get_data_arr");
3058    }
3059
3060    #[test]
3061    fn before_complete_snapshots_oldmenucmp() {
3062        // c:463 — `oldmenucmp = menucmp;`
3063        let _g = crate::ported::zle::zle_main::zle_test_setup();
3064        MENUCMP.store(7, Ordering::Relaxed);
3065        OLDMENUCMP.store(0, Ordering::Relaxed);
3066        let mut lst = 0;
3067        let _ = before_complete(&mut lst);
3068        assert_eq!(OLDMENUCMP.load(Ordering::Relaxed), 7);
3069        // Reset for other tests.
3070        MENUCMP.store(0, Ordering::Relaxed);
3071        OLDMENUCMP.store(0, Ordering::Relaxed);
3072    }
3073
3074    #[test]
3075    fn before_complete_clears_showagain() {
3076        // c:467 — `showagain = 0;` always (after the validlist gate).
3077        let _g = crate::ported::zle::zle_main::zle_test_setup();
3078        crate::ported::zle::zle_tricky::SHOWAGAIN.store(5, Ordering::Relaxed);
3079        let mut lst = 0;
3080        let _ = before_complete(&mut lst);
3081        assert_eq!(
3082            crate::ported::zle::zle_tricky::SHOWAGAIN.load(Ordering::Relaxed),
3083            0,
3084            "SHOWAGAIN must be cleared by before_complete"
3085        );
3086    }
3087
3088    #[test]
3089    fn remsquote_default_quoting() {
3090        let _g = crate::ported::zle::zle_main::zle_test_setup();
3091        let mut s = String::from("a'\\''b");
3092        let n = remsquote(&mut s);
3093        assert_eq!(s, "a'b");
3094        assert_eq!(n, 3);
3095    }
3096
3097    #[test]
3098    fn ctokenize_dollar_substitution() {
3099        let _g = crate::ported::zle::zle_main::zle_test_setup();
3100        let out = ctokenize("$x{y}");
3101        let chars: Vec<char> = out.chars().collect();
3102        assert_eq!(chars[0], Stringg);
3103        assert_eq!(chars[1], 'x');
3104        assert_eq!(chars[2], Inbrace);
3105        assert_eq!(chars[3], 'y');
3106        assert_eq!(chars[4], Outbrace);
3107    }
3108
3109    #[test]
3110    fn get_user_var_inline_list() {
3111        let _g = crate::ported::zle::zle_main::zle_test_setup();
3112        let result = get_user_var(Some("(a b c)")).unwrap();
3113        assert_eq!(result, vec!["a", "b", "c"]);
3114    }
3115
3116    #[test]
3117    fn matchcmp_str_sort_default() {
3118        let _g = crate::ported::zle::zle_main::zle_test_setup();
3119        MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
3120        let mut a = Cmatch::default(); a.str = Some("apple".into());
3121        let mut b = Cmatch::default(); b.str = Some("banana".into());
3122        assert_eq!(matchcmp(&a, &b), std::cmp::Ordering::Less);
3123        assert_eq!(matchcmp(&b, &a), std::cmp::Ordering::Greater);
3124        assert_eq!(matchcmp(&a, &a), std::cmp::Ordering::Equal);
3125        MATCHORDER.store(0, Ordering::Relaxed);
3126    }
3127
3128    #[test]
3129    fn dupmatch_clones_strings_and_truncates_braces() {
3130        let _g = crate::ported::zle::zle_main::zle_test_setup();
3131        // C body c:3370: deep-copy strings, truncate brpl/brsl to nbeg/nend.
3132        let mut src = Cmatch::default();
3133        src.str = Some("foo".into());
3134        src.ipre = Some("ipre".into());
3135        src.flags = 7;
3136        src.brpl = vec![10, 20, 30, 40];
3137        src.brsl = vec![5, 6, 7];
3138        src.qipl = 1;
3139        src.qisl = 2;
3140        src.mode = 0o755;
3141        src.modec = 'd';
3142
3143        let r = dupmatch(&src, 2, 1);
3144        assert_eq!(r.str.as_deref(), Some("foo"));
3145        assert_eq!(r.ipre.as_deref(), Some("ipre"));
3146        assert_eq!(r.flags, 7);
3147        assert_eq!(r.brpl, vec![10, 20]);      // truncated to nbeg=2
3148        assert_eq!(r.brsl, vec![5]);           // truncated to nend=1
3149        assert_eq!(r.qipl, 1);
3150        assert_eq!(r.qisl, 2);
3151        assert_eq!(r.mode, 0o755);
3152        assert_eq!(r.modec, 'd');
3153    }
3154
3155    #[test]
3156    fn dupmatch_empty_braces_stay_empty() {
3157        let _g = crate::ported::zle::zle_main::zle_test_setup();
3158        // C body c:3395/3404: NULL brpl/brsl stay NULL regardless of nbeg/nend.
3159        let src = Cmatch::default();
3160        let r = dupmatch(&src, 5, 5);
3161        assert!(r.brpl.is_empty());
3162        assert!(r.brsl.is_empty());
3163    }
3164
3165    #[test]
3166    fn makearray_sorted_and_deduped() {
3167        let _g = crate::ported::zle::zle_main::zle_test_setup();
3168        // c:3262-3291: sort + dedup with matcheq. Same str + nil disp =>
3169        // collapses into one entry with CMF_FMULT set on the survivor.
3170        let mut a = Cmatch::default(); a.str = Some("z".into());
3171        let mut b = Cmatch::default(); b.str = Some("a".into());
3172        let mut c = Cmatch::default(); c.str = Some("a".into());
3173        let (arr, n, _nl, _ll) = makearray(vec![a, b, c], CGF_MATSORT);
3174        // Two distinct visible strings after dedup ("a", "z").
3175        assert_eq!(arr.len(), 2);
3176        assert_eq!(n, 2);
3177        assert_eq!(arr[0].str.as_deref(), Some("a"));
3178        assert_eq!(arr[1].str.as_deref(), Some("z"));
3179    }
3180
3181    #[test]
3182    fn makearray_nosort_unchanged_order() {
3183        let _g = crate::ported::zle::zle_main::zle_test_setup();
3184        // c:3300: CGF_NOSORT branch; with no UNIQ flags, order preserved.
3185        let mut a = Cmatch::default(); a.str = Some("z".into());
3186        let mut b = Cmatch::default(); b.str = Some("a".into());
3187        let (arr, n, _, _) = makearray(vec![a, b], CGF_NOSORT | CGF_UNIQALL);
3188        // UNIQALL active so no dedup pass runs.
3189        assert_eq!(n, 2);
3190        assert_eq!(arr[0].str.as_deref(), Some("z"));
3191        assert_eq!(arr[1].str.as_deref(), Some("a"));
3192    }
3193
3194    #[test]
3195    fn makearray_strings_dedup_consecutive() {
3196        let _g = crate::ported::zle::zle_main::zle_test_setup();
3197        // c:3239 path: sort + drop adjacent duplicates.
3198        let (arr, n) = makearray_strings(
3199            vec!["b".into(), "a".into(), "a".into(), "c".into()],
3200            1,
3201        );
3202        assert_eq!(n, 3);
3203        assert_eq!(arr, vec!["a", "b", "c"]);
3204    }
3205
3206    #[test]
3207    fn check_param_no_dollar_returns_none() {
3208        let _g = crate::ported::zle::zle_main::zle_test_setup();
3209        // c:1316: no `$` in string → return None.
3210        OFFS.store(2, Ordering::Relaxed);
3211        assert_eq!(check_param("abc", false, false), None);
3212    }
3213
3214    #[test]
3215    fn check_param_simple_dollar_var_at_cursor() {
3216        let _g = crate::ported::zle::zle_main::zle_test_setup();
3217        // c:1259-1311: `$FOO` with cursor inside the name → return b.
3218        OFFS.store(2, Ordering::Relaxed);
3219        let s = format!("{}FOO", crate::ported::zsh_h::Stringg);
3220        let r = check_param(&s, false, true);
3221        assert!(r.is_some(), "expected Some(b) inside $FOO");
3222    }
3223
3224    #[test]
3225    fn callcompfunc_empty_fn_no_panic() {
3226        let _g = crate::ported::zle::zle_main::zle_test_setup();
3227        // c:552: getshfunc(NULL) early-return.
3228        callcompfunc("anything", "");
3229    }
3230
3231    #[test]
3232    fn callcompfunc_sets_compstate_context() {
3233        let _g = crate::ported::zle::zle_main::zle_test_setup();
3234        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3235        // c:619: context selection — verified via the pure
3236        // compcontext_for helper (callcompfunc calls it and writes
3237        // to paramtab via setsparam, but paramtab read-back in a
3238        // unit-test context without a live VM is unreliable).
3239        ispar.store(0, Ordering::Relaxed);
3240        linwhat.store(IN_PAR_LW, Ordering::Relaxed);
3241        assert_eq!(compcontext_for("foo"), "assign_parameter");
3242        // Body executes without panicking against the real paramtab.
3243        callcompfunc("foo", "_test_fn");
3244    }
3245
3246    /// Test-only serializer for tests that mutate file-scope globals.
3247    static GLOBAL_MUT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3248
3249    #[test]
3250    fn compcontext_for_routes_ispar_first() {
3251        let _g = crate::ported::zle::zle_main::zle_test_setup();
3252        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3253        ispar.store(2, Ordering::Relaxed);
3254        linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
3255        assert_eq!(compcontext_for("x"), "brace_parameter");
3256        ispar.store(1, Ordering::Relaxed);
3257        assert_eq!(compcontext_for("x"), "parameter");
3258        ispar.store(0, Ordering::Relaxed);
3259        linwhat.store(IN_MATH_LW, Ordering::Relaxed);
3260        assert_eq!(compcontext_for("x"), "math");
3261        linwhat.store(IN_COND_LW, Ordering::Relaxed);
3262        assert_eq!(compcontext_for("x"), "condition");
3263        linwhat.store(IN_ENV_LW, Ordering::Relaxed);
3264        assert_eq!(compcontext_for("x"), "value");
3265        linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
3266        assert_eq!(compcontext_for("x"), "command");
3267    }
3268
3269    #[test]
3270    fn addmatches_empty_argv_early_return() {
3271        let _g = crate::ported::zle::zle_main::zle_test_setup();
3272        // c:2138-2139: empty argv + dummies==0 + no CAF_ALL → return 1.
3273        let mut dat = crate::ported::zle::comp_h::Cadata::default();
3274        dat.dummies = 0;
3275        dat.aflags = 0;
3276        assert_eq!(addmatches(&mut dat, &[]), 1);
3277    }
3278
3279    #[test]
3280    fn addmatches_appends_argv_to_default_group() {
3281        let _g = crate::ported::zle::zle_main::zle_test_setup();
3282        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3283        // c:2200 simplified body: each argv entry → addmatch into "default" group.
3284        amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3285        matches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3286        let mut dat = crate::ported::zle::comp_h::Cadata::default();
3287        dat.dummies = -1;
3288        let _ = addmatches(&mut dat, &["a".into(), "b".into()]);
3289        let n = matches.get().unwrap().lock().unwrap().len();
3290        assert!(n >= 2);
3291    }
3292
3293    #[test]
3294    fn add_match_data_returns_populated_cmatch() {
3295        let _g = crate::ported::zle::zle_main::zle_test_setup();
3296        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3297        // c:3052-3067: cm.str/orig/pre/suf populated; mnum bumps by 1.
3298        matches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3299        let before = mnum.load(Ordering::Relaxed);
3300        let cm = add_match_data(
3301            0, "match", "match-orig", None,
3302            "ipre", "ripre", "isuf",
3303            "pre", "prpre", "ppre", None,
3304            "psuf", None,
3305            "suf", 0, 0,
3306        );
3307        assert_eq!(cm.str.as_deref(), Some("match"));
3308        assert_eq!(cm.orig.as_deref(), Some("match-orig"));
3309        assert_eq!(cm.pre.as_deref(),  Some("pre"));
3310        assert_eq!(cm.suf.as_deref(),  Some("suf"));
3311        assert_eq!(mnum.load(Ordering::Relaxed), before + 1);
3312    }
3313
3314    #[test]
3315    fn add_match_data_exact_records_into_ainfo() {
3316        let _g = crate::ported::zle::zle_main::zle_test_setup();
3317        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3318        // c:3060-3062: exact != 0 writes ai.exact/exactm.
3319        if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3320            *g = Some(Aminfo::default());
3321        }
3322        let _ = add_match_data(
3323            0, "x", "x", None, "", "", "", "", "", "", None, "", None, "", 0, 1,
3324        );
3325        let a = ainfo.get().unwrap().lock().unwrap().clone().unwrap();
3326        assert_eq!(a.exact, 1);
3327        assert!(a.exactm.is_some());
3328    }
3329
3330    #[test]
3331    fn set_comp_sep_returns_one() {
3332        let _g = crate::ported::zle::zle_main::zle_test_setup();
3333        // c:1937: stubbed body returns 1 (no-change marker).
3334        assert_eq!(set_comp_sep(), 1);
3335    }
3336
3337    #[test]
3338    fn foredel_deletes_forward_from_zlemetacs() {
3339        let _g = crate::ported::zle::zle_main::zle_test_setup();
3340        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3341        // zle_utils.c:1105 — delete `ct` chars forward from ZLEMETACS.
3342        if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
3343            *g = "abcdef".to_string();
3344        }
3345        ZLEMETACS.store(2, Ordering::Relaxed);
3346        ZLEMETALL.store(6, Ordering::Relaxed);
3347        foredel(3);
3348        let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
3349        assert_eq!(line, "abf");
3350        assert_eq!(ZLEMETALL.load(Ordering::Relaxed), 3);
3351    }
3352
3353    #[test]
3354    fn inststr_inserts_at_zlemetacs() {
3355        let _g = crate::ported::zle::zle_main::zle_test_setup();
3356        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3357        // zle_tricky.c:278 — insert at cursor.
3358        if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
3359            *g = "hello".to_string();
3360        }
3361        ZLEMETACS.store(5, Ordering::Relaxed);
3362        ZLEMETALL.store(5, Ordering::Relaxed);
3363        inststr(" world");
3364        let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
3365        assert_eq!(line, "hello world");
3366        assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 11);
3367    }
3368
3369    #[test]
3370    fn metafy_and_unmetafy_roundtrip_globals() {
3371        let _g = crate::ported::zle::zle_main::zle_test_setup();
3372        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3373        // zle_tricky.c:978,995 — meta/unmeta operate on the global pair.
3374        if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
3375            *g = "plain ascii".to_string();
3376        }
3377        ZLECS.store(3, Ordering::Relaxed);
3378        ZLELL.store(11, Ordering::Relaxed);
3379        metafy_line();
3380        // For ASCII input the meta form equals the raw form.
3381        assert_eq!(
3382            ZLEMETALINE.get().unwrap().lock().unwrap().clone(),
3383            "plain ascii"
3384        );
3385        assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 3);
3386        unmetafy_line();
3387        assert_eq!(
3388            ZLELINE.get().unwrap().lock().unwrap().clone(),
3389            "plain ascii"
3390        );
3391    }
3392
3393    #[test]
3394    fn selfinsert_appends_lastchar_at_zlecs() {
3395        let _g = crate::ported::zle::zle_main::zle_test_setup();
3396        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3397        // zle_misc.c:112-141 — insert one char at cursor, bump zlecs.
3398        if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
3399            *g = "ab".to_string();
3400        }
3401        ZLECS.store(2, Ordering::Relaxed);
3402        ZLELL.store(2, Ordering::Relaxed);
3403        LASTCHAR.store(b'c' as i32, Ordering::Relaxed);
3404        let rv = selfinsert();
3405        assert_eq!(rv, 0);
3406        assert_eq!(ZLELINE.get().unwrap().lock().unwrap().clone(), "abc");
3407        assert_eq!(ZLECS.load(Ordering::Relaxed), 3);
3408    }
3409
3410    #[test]
3411    fn minfo_clear_and_asked_zero_mutate_state() {
3412        let _g = crate::ported::zle::zle_main::zle_test_setup();
3413        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3414        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() {
3415            let mut cm = Cmatch::default();
3416            cm.str = Some("x".into());
3417            g.cur = Some(Box::new(cm));
3418            g.asked = 1;
3419        }
3420        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; }
3421        if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.asked = 0; }
3422        let m = MINFO.get().unwrap().lock().unwrap().clone();
3423        assert!(m.cur.is_none());
3424        assert_eq!(m.asked, 0);
3425    }
3426
3427    #[test]
3428    fn cline_matched_stub_marks_node() {
3429        let _g = crate::ported::zle::zle_main::zle_test_setup();
3430        // compmatch.c:253 — sets CLF_MATCHED on the node chain. We
3431        // verify by running through the stub on a non-empty string
3432        // without panicking and trusting compmatch's body for the
3433        // actual flag set.
3434        cline_matched_compcore(Some("foo"));
3435        cline_matched_compcore(None);
3436        cline_matched_compcore(Some(""));
3437    }
3438
3439    #[test]
3440    fn permmatches_returns_fi_zero_when_count_present() {
3441        let _g = crate::ported::zle::zle_main::zle_test_setup();
3442        let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3443        // c:3444-3447: if ainfo->count is non-zero, fi stays 0.
3444        amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3445        pmatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3446        if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3447            *a = Some(Aminfo { count: 5, ..Default::default() });
3448        }
3449        newmatches.store(1, Ordering::Relaxed);
3450        let fi = permmatches(0);
3451        assert_eq!(fi, 0);
3452        assert_eq!(hasperm.load(Ordering::Relaxed), 1);
3453    }
3454}