zshrs 0.12.60

The first JIT-compiled Unix shell — bytecode VM, Cranelift JIT, worker pool, AOP intercept, Rkyv caching
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
//! Tiny helpers shared between per-fn ports. Kept here (not in
//! library.rs) so the `ported/` tree stands alone.

use std::path::Path;

// =====================================================================
// Function-local parameter declarations for the Rust ports.
// =====================================================================
//
// An upstream completion function opens with a `local`/`typeset` line
// (`Completion/Base/Core/_main_complete:11,27-54`). Every scratch name
// it touches is therefore created at `locallevel`, reads back as
// `…-local` through `${(t)name}`, and is unwound by `endparamscope`
// when the function returns.
//
// A Rust port assigns the same names with `setsparam`/`setaparam`,
// which routes through `createparam(name, PM_SCALAR)` — no PM_LOCAL,
// so the parameter is born at level 0 and both properties are lost:
// `${(t)_comp_tags}` reads `scalar` and the name survives the call.
// That is observable: the user's `_parameters`
// (`~/.zpwr/autoload/comp_utils/_parameters:34`) filters candidates
// with `${(@k)parameters[(R)${pattern[2]}~*local*]}`, i.e. it drops
// every parameter whose type string contains `local`. Leaked port
// scratch names slipped through that filter and `unset <TAB>` offered
// them alongside the user's real parameters.
//
// `declare_locals` is the one place that gap is closed: it is the
// Rust-port spelling of the upstream `local NAME …` line and mirrors
// the PM_LOCAL branch of `bin_typeset` (`src/ported/builtin.rs:5570`,
// port of `Src/builtin.c:2469-2575`).

pub use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED, PM_INTEGER, PM_READONLY, PM_UNIQUE};

/// Declare `names` local to the CURRENT function scope — the Rust-port
/// equivalent of an upstream completion function's `local NAME …` line.
///
/// `kind` carries the type/attribute bits the shell source spells out
/// (`PM_ARRAY` for `local -a`, `PM_HASHED` for `local -A`, `PM_UNIQUE`
/// for `typeset -U`); pass `0` for a plain scalar `local`.
///
/// Mirrors `Src/builtin.c:2469-2575` (`typeset_single`'s PM_LOCAL arm):
/// only allocate a shadow when the visible parameter lives at a LOWER
/// scope than the current `locallevel`, then stamp `pm->level =
/// locallevel` so `endparamscope` unwinds it. At top level
/// (`locallevel == 0`) C's `pm->level < locallevel` can never hold, so
/// nothing is declared — the port then behaves exactly as it does today
/// when run outside a function (unit tests, `--doctor`).
pub fn declare_locals(names: &[&str], kind: u32) {
    use crate::ported::params::{createparam, locallevel, paramtab};
    use crate::ported::zsh_h::{PM_HIDE, PM_LOCAL, PM_SPECIAL};
    use std::sync::atomic::Ordering;

    let cur = locallevel.load(Ordering::Relaxed); // c:2469 locallevel
    if cur == 0 {
        return;
    }
    for name in names {
        // c:2469 — `(!pm || pm->level < locallevel)`.
        //
        // The same table read also settles `newspecial`. c:2083-2085:
        //   if ((pm->node.flags & PM_SPECIAL)
        //       && !(on & PM_HIDE) && !(pm->node.flags & PM_HIDE & ~off))
        //       newspecial = NS_NORMAL;
        // i.e. localizing a PM_SPECIAL parameter keeps it special unless
        // `-h` hides it, on either the special itself or this statement.
        let (needs_shadow, newspecial) = paramtab()
            .read()
            .ok()
            .and_then(|t| {
                t.get(*name).map(|pm| {
                    let special = (pm.node.flags as u32 & PM_SPECIAL) != 0
                        && (kind & PM_HIDE) == 0
                        && (pm.node.flags as u32 & PM_HIDE) == 0;
                    (pm.level < cur, special)
                })
            })
            .unwrap_or((true, false));
        if !needs_shadow {
            continue;
        }
        // c:2470 — `createparam(pname, on | PM_LOCAL)`.
        let _ = createparam(name, (kind | PM_LOCAL) as i32);
        // c:2575 — `else if (on & PM_LOCAL) pm->level = locallevel;`
        // plus the attribute stamp so `typeset -U` keeps PM_UNIQUE.
        if let Ok(mut tab) = paramtab().write() {
            if let Some(pm) = tab.get_mut(*name) {
                pm.level = cur;
                pm.node.flags |= (kind & PM_UNIQUE) as i32;
                // c:2425 — `pm->node.flags = (PM_TYPE(pm->node.flags) | on
                // | PM_SPECIAL) & ~off;`. `createparam` deliberately drops
                // the bit (`Src/params.c:1174` stores `flags & ~PM_LOCAL`
                // on the fresh struct), so the special-ness of the shadowed
                // parameter has to be re-stamped here the way the
                // `newspecial` arm of `typeset_single` does. Without it
                // `integer SECONDS=0` (_main_complete sh:162) read
                // `integer-local` where zsh reads `integer-local-special`,
                // and `_parameters` — which drops every candidate whose
                // type matches `*local*` — mis-classified the shadow.
                if newspecial {
                    pm.node.flags |= PM_SPECIAL as i32;
                }
            }
        }
    }
}

/// A parameter scope for a Rust port that is invoked as a DIRECT Rust
/// call rather than through `dispatch_function_call`.
///
/// `declare_locals` only stamps `pm->level = locallevel`; the unwind is
/// `endparamscope`'s job, and that runs from `doshfunc`. A port reached
/// by a plain Rust call (`_alternative` -> `_tags(&…)` /
/// `_next_label(&…)` -> `_description(&…)`) therefore never gets one, so
/// every name in its `declare_locals` list stayed shadowed for the rest
/// of the CALLER's body.
///
/// Concretely: `_tags` declares `tmp` and `_description` declares
/// `opts`, and both names are `_files`' own locals holding the results
/// of its `zparseopts -a opts '/=tmp' 'g+:-=tmp' … W: …` line. After
/// `_alternative` ran either port, `_files` saw `opts=()` / `tmp=()`, so
/// `-W /dev` and `-g '*(-%b,-/)'` were both dropped — `mount /dev/<TAB>`
/// listed every file in `$PWD` and `PATH=…:<TAB>` listed files instead
/// of directories.
///
/// Holding one of these for the port's body reproduces the visible half
/// of what a real shell function gets from `endparamscope`
/// (`Src/params.c:5867-5933`, the `pm->level > locallevel` arm): each
/// declared name is put back exactly as the caller left it.
///
/// It restores by NAME rather than by bumping `locallevel` and calling
/// `endparamscope`, because a port's body also writes caller-visible
/// state (`_comp_tags`, `curtag`, the `expl` array named by
/// `_description`'s `$2`). A whole-scope unwind takes those with it —
/// `_tags` then reported "comptags: no tags registered" for every
/// context.
pub struct LocalScope {
    saved: Vec<(String, Option<Box<crate::ported::zsh_h::param>>)>,
}

impl LocalScope {
    /// Declare `names` local (see [`declare_locals`]) and remember what
    /// each one looked like beforehand.
    pub fn declare(names: &[&str], kind: u32) -> Self {
        let mut scope = LocalScope { saved: Vec::new() };
        scope.also(names, kind);
        scope
    }

    /// Add more names to an existing scope — the port equivalent of a
    /// second `local -a …` line.
    pub fn also(&mut self, names: &[&str], kind: u32) {
        if let Ok(tab) = crate::ported::params::paramtab().read() {
            for name in names {
                self.saved
                    .push(((*name).to_string(), tab.get(*name).cloned()));
            }
        }
        declare_locals(names, kind);
    }

    /// `local NAME="$NAME"` — see [`declare_locals_keeping_value`].
    pub fn also_keeping_value(&mut self, names: &[&str]) {
        if let Ok(tab) = crate::ported::params::paramtab().read() {
            for name in names {
                self.saved
                    .push(((*name).to_string(), tab.get(*name).cloned()));
            }
        }
        declare_locals_keeping_value(names);
    }
}

impl Drop for LocalScope {
    fn drop(&mut self) {
        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
            for (name, prev) in self.saved.iter().rev() {
                match prev {
                    Some(pm) => {
                        tab.insert(name.clone(), pm.clone());
                    }
                    None => {
                        tab.remove(name);
                    }
                }
            }
        }
    }
}

/// `typeset -r NAME` applied AFTER the value is in place — the second
/// half of an upstream `local -ar NAME=(…)` / `local -r NAME=…` line.
///
/// [`declare_locals`] cannot carry `PM_READONLY` itself: `createparam`
/// stamps the bit immediately, and the port assigns the value on the
/// NEXT statement, so the assignment would be rejected as a write to a
/// read-only parameter. Upstream has no such split — `local -ar x=(…)`
/// is one operation whose value lands before the bit does — so the port
/// declares, assigns, then calls this.
///
/// Mirrors the `PM_READONLY` arm of `typeset_single`
/// (`Src/builtin.c:2469-2575`): the bit is OR'd onto the existing
/// `pm->node.flags`, and because the param already lives at
/// `locallevel`, `endparamscope` unwinds it with the rest of the scope.
///
/// Skipped at `locallevel == 0` for the same reason [`declare_locals`]
/// returns early there: with no function scope there is no shadow to
/// stamp and no `endparamscope` to unstamp it, so the bit would pin the
/// caller's GLOBAL parameter read-only forever — the next completion's
/// own assignment would then fail with "read-only variable". Upstream
/// cannot reach that state at all: `local -ar` is a syntax error outside
/// a function.
pub fn mark_readonly(names: &[&str]) {
    use crate::ported::params::{locallevel, paramtab};
    use crate::ported::zsh_h::PM_READONLY;
    use std::sync::atomic::Ordering;
    if locallevel.load(Ordering::Relaxed) == 0 {
        return;
    }
    if let Ok(mut tab) = paramtab().write() {
        for name in names {
            if let Some(pm) = tab.get_mut(*name) {
                pm.node.flags |= PM_READONLY as i32;
            }
        }
    }
}

// =====================================================================
// Boolean style tests — `zstyle -t` / `zstyle -T`.
// =====================================================================
//
// !!! WARNING: RUST-ONLY HELPERS !!!
//
// These two have no C counterpart, because upstream has no function to
// port here: every compsys boolean style test is literally the COMMAND
// `zstyle -t "$ctx" <style>` (or `-T`) in the shell source, with the
// arms below it reading `$?`. The helpers run that same builtin —
// [`bin_zstyle`](crate::ported::modules::zutil::bin_zstyle), the port of
// `Src/Modules/zutil.c:487` — so the tri-state exit is the builtin's
// own rather than a re-derivation of it.
//
// They exist because the obvious-looking
// [`testforstyle`](crate::ported::modules::zutil::testforstyle)
// (`Src/Modules/zutil.c:465`) is NOT `zstyle -t`. It is the primitive
// behind `zstyle -q` (c:749-756) and answers "is this style DEFINED for
// this context", ignoring the value entirely — so
// `zstyle ':completion:*' <style> 0` made every port that called it take
// the TRUE branch, the exact opposite of what the style asks for. The
// same misport was measured and fixed at `_setup`'s `last-prompt`
// (e3f05f5c05); these helpers are that fix generalised so the shape is
// written once instead of once per completer.
//
// The exits are the `case 't': case 'T':` arm at c:701-724:
//
//   * 0 — style set for the context AND its first value is one of
//     `true` / `yes` / `on` / `1` (c:719-722).
//   * 1 — style set for the context but its first value is not one of
//     those (c:719-722), or set with NO values (c:724 `vals ? 1 : 2`).
//   * 2 — no style pattern matched this context (c:724 `: 2`), for `-t`
//     only; `-T` returns 0 there instead (c:724 `: 0`), which is the
//     whole difference between the two letters.

/// Build the empty `options` struct `bin_zstyle` wants. It parses
/// `args[0]` itself (the BUILTIN spec carries a NULL optstr at c:2139),
/// so nothing needs to be pre-set here.
fn empty_ops() -> crate::ported::zsh_h::options {
    crate::ported::zsh_h::options {
        ind: [0u8; crate::ported::zsh_h::MAX_OPS],
        args: Vec::new(),
        argscount: 0,
        argsalloc: 0,
    }
}

/// `zstyle -t <ctx> <style>` — 0 boolean-true, 1 set-but-not-true,
/// 2 unset for this context. See the block comment above.
pub fn zstyle_t(ctx: &str, style: &str) -> i32 {
    crate::ported::modules::zutil::bin_zstyle(
        "zstyle",
        &["-t".to_string(), ctx.to_string(), style.to_string()],
        &empty_ops(),
        0,
    )
}

/// `zstyle -T <ctx> <style>` — as [`zstyle_t`], except that an UNSET
/// style is true (0) rather than 2 (c:724). This is the "default yes"
/// spelling upstream uses for styles like `add-space` and `verbose`.
#[allow(non_snake_case)]
pub fn zstyle_T(ctx: &str, style: &str) -> i32 {
    crate::ported::modules::zutil::bin_zstyle(
        "zstyle",
        &["-T".to_string(), ctx.to_string(), style.to_string()],
        &empty_ops(),
        0,
    )
}

/// Hand `args` to `bin_zparseopts` through its `-v <name>` source array,
/// declared LOCAL to the enclosing function scope first.
///
/// zsh's `zparseopts` has no `-v`: upstream reads the positional list, so
/// there is no `sh:NN local __compsys_argv` line to port. The bridge array
/// exists only because `bin_zparseopts` takes its argv from `paramtab` by
/// name, and it cannot be dropped without rewriting that builtin's entry
/// point — the DESTINATION arrays (`-a __gopt`, …) go through `paramtab`
/// too. What it must not be is GLOBAL.
///
/// A completer is allowed to turn `WARN_CREATE_GLOBAL` on for its own body
/// (`~/.zinit/completions/_mc:36` — `setopt localoptions warncreateglobal
/// typesetsilent` — the house style of the zsh-completions collection;
/// `compinit`'s `_comp_options` only turns it OFF for the utility functions
/// that do not opt back in). Every port reached from such a completer then
/// printed one `_requested: array parameter __compsys_argv created globally
/// in function _requested` per call, on the terminal, in place of the match
/// list.
///
/// The destination arrays escaped the same diagnostic only by accident: they
/// are left behind after the call, so `createparam` reports `created == 0`
/// from the second completion onwards and `check_warn_pm`
/// (`src/ported/params.rs:6669`) returns early. `__compsys_argv` is unset
/// after every call — the tidier lifetime — so it was re-created, and warned
/// about, every single time.
pub fn set_bridge_argv(name: &str, args: &[String]) {
    declare_locals(&[name], PM_ARRAY);
    let _ = crate::ported::params::setaparam(name, args.to_vec());
}

/// `local NAME="$NAME"` — declare `names` local while carrying the
/// enclosing scope's scalar value into the shadow.
///
/// Upstream spells this out where the completer chain must keep
/// reading an inherited value it is also allowed to overwrite:
/// `_main_complete:31` (`curcontext="$curcontext"`), `_tags:19`,
/// `_dispatch:4`. A bare [`declare_locals`] would hand the port an
/// empty parameter instead.
pub fn declare_locals_keeping_value(names: &[&str]) {
    for name in names {
        let inherited = crate::ported::params::getsparam(name);
        declare_locals(&[name], 0);
        if let Some(v) = inherited {
            let _ = crate::ported::params::setsparam(name, &v);
        }
    }
}
/// The directory list `compinit` must scan: `$fpath` as it stands at
/// call time (`Completion/compinit:523` `for _i_dir in $fpath`, and
/// `compaudit` at sh:455), falling back to `env_fpath` when the array is
/// unset or empty.
///
/// `ShellExecutor::fpath` (vm_helper.rs:532) is seeded once at startup
/// from `$FPATH` (vm_helper.rs:1174/1287) and never resynced, so the
/// `fpath=( … )` line that precedes `compinit` in every .zshrc was
/// invisible to the scan. With `$FPATH` exported the two agreed by
/// accident; without it — `zsh -f`, a login shell that builds `fpath` in
/// .zshrc, the parity harness's child env — the scan got ZERO
/// directories, and the worker's empty result was then written over the
/// completion cache, leaving `$_comps` empty and every command falling
/// through to `-default-`.
pub fn compinit_scan_dirs(env_fpath: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
    match crate::ported::params::getaparam("fpath") {
        Some(live) if !live.is_empty() => live.iter().map(std::path::PathBuf::from).collect(),
        _ => env_fpath.to_vec(),
    }
}

/// `is_executable` — see implementation.
pub fn is_executable(path: &Path) -> bool {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        if let Ok(meta) = path.metadata() {
            let mode = meta.permissions().mode();
            return mode & 0o111 != 0;
        }
    }
    #[cfg(not(unix))]
    {
        if let Some(ext) = path.extension() {
            let ext = ext.to_string_lossy().to_lowercase();
            return matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com");
        }
    }
    false
}

/// Shell-glob matcher — supports `*`, `?`, and `(a|b|c)`
/// alternation (zsh extended-glob's `(…|…)` form). Sufficient for
/// the patterns end-user completion files use (e.g.
/// `*.(md|rs|toml)` from `_suffix_alias_files`).
pub fn glob_matches(pattern: &str, text: &str) -> bool {
    // Handle leading `(alt1|alt2|…)` at the top level — split at the
    // matching close paren, try each alternative concatenated with
    // the remainder.
    if let Some(rest) = pattern.strip_prefix('(') {
        if let Some(close) = find_top_close_paren(rest) {
            let group = &rest[..close];
            let after = &rest[close + 1..];
            return group.split('|').any(|alt| {
                let combined = format!("{}{}", alt, after);
                glob_matches(&combined, text)
            });
        }
    }
    let pat: Vec<char> = pattern.chars().collect();
    let txt: Vec<char> = text.chars().collect();
    glob_helper(&pat, &txt)
}

fn find_top_close_paren(s: &str) -> Option<usize> {
    let mut depth: i32 = 1;
    for (i, c) in s.char_indices() {
        match c {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    return Some(i);
                }
            }
            _ => {}
        }
    }
    None
}

fn glob_helper(pat: &[char], txt: &[char]) -> bool {
    if pat.is_empty() {
        return txt.is_empty();
    }
    // Inline alternation at any position: when we encounter `(...)`,
    // re-route through `glob_matches` on the remainder.
    if pat[0] == '(' {
        let rest: String = pat[1..].iter().collect();
        let txt_str: String = txt.iter().collect();
        if let Some(close) = find_top_close_paren(&rest) {
            let group = &rest[..close];
            let after = &rest[close + 1..];
            return group.split('|').any(|alt| {
                let combined = format!("{}{}", alt, after);
                glob_matches(&combined, &txt_str)
            });
        }
    }
    match pat[0] {
        '*' => {
            for i in 0..=txt.len() {
                if glob_helper(&pat[1..], &txt[i..]) {
                    return true;
                }
            }
            false
        }
        '?' => !txt.is_empty() && glob_helper(&pat[1..], &txt[1..]),
        c => !txt.is_empty() && txt[0] == c && glob_helper(&pat[1..], &txt[1..]),
    }
}

/// Shell-glob matcher mirror of the helper that used to live in
/// `compsys/functions.rs` — kept as a separate symbol because callers
/// were spelled `functions::glob_match(...)`, distinct from
/// `glob_matches` above (which the `library.rs`/`ported/_path_files`
/// code used). Both share semantics; the duplicate is intentional for
/// API-shape compat with both call-site ()/* styles */.
pub fn glob_match(pattern: &str, text: &str) -> bool {
    glob_matches(pattern, text)
}

/// Levenshtein edit distance, used by `_approximate`, `_correct`,
/// `_correct_filename`, and `_correct_word`. Moved out of
/// `compsys/functions.rs` so it can be shared across the per-fn ports
/// without introducing a circular dependency between them.
pub fn edit_distance(a: &str, b: &str) -> usize {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let m = a_chars.len();
    let n = b_chars.len();

    let mut dp = vec![vec![0; n + 1]; m + 1];

    // Levenshtein DP base row/col init — needless_range_loop trips here
    // but the index IS the value being written, not a positional access.
    #[allow(clippy::needless_range_loop)]
    for i in 0..=m {
        dp[i][0] = i;
    }
    #[allow(clippy::needless_range_loop)]
    for j in 0..=n {
        dp[0][j] = j;
    }

    for i in 1..=m {
        for j in 1..=n {
            let cost = if a_chars[i - 1] == b_chars[j - 1] {
                0
            } else {
                1
            };
            dp[i][j] = (dp[i - 1][j] + 1)
                .min(dp[i][j - 1] + 1)
                .min(dp[i - 1][j - 1] + cost);
        }
    }

    dp[m][n]
}

/// Check if a string matches any ignored pattern. Extracted from
/// `compsys/base.rs::is_ignored`. Uses the same `glob_match` helper
/// as the rest of the per-fn ports.
pub fn is_ignored(s: &str, patterns: &[String]) -> bool {
    for pattern in patterns {
        if glob_match(pattern, s) {
            return true;
        }
    }
    false
}

/// `get_ignored_patterns(context)` — collect `ignored-patterns`
/// zstyle values for `context` via the real `lookupstyle` in
/// `src/ported/modules/zutil.rs`.
pub fn get_ignored_patterns(context: &str) -> Vec<String> {
    crate::ported::modules::zutil::lookupstyle(context, "ignored-patterns")
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The zparseopts bridge array must be a FUNCTION LOCAL, not a global.
    ///
    /// `__compsys_argv` is written by 18 `run_*` helpers under
    /// `src/compsys/ported/`, once per call, and unset again straight after —
    /// so every call RE-CREATES it. `createparam` at `locallevel == 0` makes
    /// that a global creation, and `check_warn_pm`
    /// (`src/ported/params.rs:6669`, port of `Src/params.c:3158`) prints
    /// `<caller>: array parameter __compsys_argv created globally in function
    /// <caller>` for each one whenever `WARN_CREATE_GLOBAL` is on.
    ///
    /// It is on more often than the option's rarity suggests: `compinit`'s
    /// `_comp_options` clears it (sh:171 `NO_warncreateglobal`), but a
    /// completer is free to set it back for its own body, and the
    /// zsh-completions house style does exactly that
    /// (`_mc:36` — `setopt localoptions warncreateglobal typesetsilent`).
    /// `mc <TAB>` then drew 29 rows of diagnostics instead of its match list,
    /// and `mc -<TAB>` 40.
    ///
    /// Asserting the TYPE STRING rather than `pm.level` is deliberate: it is
    /// the same `${(t)name}` text `_parameters` filters on, so this test also
    /// pins the property that made the earlier leaks visible as bogus
    /// completion matches.
    #[test]
    fn bridge_argv_is_declared_local_not_created_global() {
        let _g = crate::test_util::global_state_lock();
        crate::ported::utils::inc_locallevel();
        let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            set_bridge_argv("__compsys_argv", &["-J".to_string(), "grp".to_string()]);
            let ty = crate::ported::params::paramtab()
                .read()
                .ok()
                .and_then(|t| {
                    t.get("__compsys_argv")
                        .map(|pm| crate::ported::modules::parameter::paramtypestr(pm))
                })
                .unwrap_or_default();
            assert_eq!(
                ty, "array-local",
                "bridge argv must be `local -a`; `array` means every call \
                 re-creates a global and WARN_CREATE_GLOBAL prints a line"
            );
            assert_eq!(
                crate::ported::params::getaparam("__compsys_argv").unwrap_or_default(),
                vec!["-J".to_string(), "grp".to_string()],
                "declaring it local must not cost the value zparseopts reads"
            );
        }));
        crate::ported::params::endparamscope();
        let _ = crate::ported::params::unsetparam("__compsys_argv");
        if let Err(p) = out {
            std::panic::resume_unwind(p);
        }
    }

    /// The type/attribute bits an upstream `local` line spells have to reach
    /// `createparam`, and the names have to be GONE after `endparamscope`.
    ///
    /// This is the substrate the whole compsys scratch-parameter fix rests on:
    /// 95 ports call `declare_locals` with the kind their shell source spells
    /// (`0` for a bare `local`, PM_ARRAY for `local -a`, PM_HASHED for
    /// `local -A`, PM_HIDE for `local -H`, PM_ARRAY|PM_UNIQUE for
    /// `local -aU`). Two things can go wrong independently and this pins both:
    ///
    ///   * the bits are dropped, so `${(t)name}` reads `scalar-local` where zsh
    ///     reads `array-local` — the port then works by luck, because the first
    ///     `setaparam` retypes it, and `_parameters`' type filter still sees a
    ///     different string than zsh does;
    ///   * `pm->level` is not stamped, so `endparamscope` leaves the name
    ///     behind and one TAB puts a completer's working variable in the user's
    ///     interactive shell.
    ///
    /// Names are prefixed so this test cannot collide with a real parameter or
    /// with another test in the same process.
    #[test]
    fn declare_locals_carries_the_shell_kind_and_unwinds() {
        let _g = crate::test_util::global_state_lock();
        crate::ported::utils::inc_locallevel();
        let cases: [(&str, u32, &str); 4] = [
            ("zzlk_scalar", 0, "scalar"),
            ("zzlk_array", PM_ARRAY, "array"),
            ("zzlk_assoc", PM_HASHED, "association"),
            ("zzlk_uniq", PM_ARRAY | PM_UNIQUE, "unique"),
        ];
        let inner = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            for (name, kind, want) in cases {
                declare_locals(&[name], kind);
                let ty = crate::ported::params::paramtab()
                    .read()
                    .ok()
                    .and_then(|t| {
                        t.get(name)
                            .map(|pm| crate::ported::modules::parameter::paramtypestr(pm))
                    })
                    .unwrap_or_default();
                assert!(
                    ty.contains(want) && ty.contains("local"),
                    "declare_locals({name}, {kind:#x}) produced `{ty}`, \
                     expected it to contain `{want}` and `local`"
                );
            }
        }));
        crate::ported::params::endparamscope();
        let survivors: Vec<&str> = cases
            .iter()
            .map(|(n, _, _)| *n)
            .filter(|n| {
                crate::ported::params::paramtab()
                    .read()
                    .map(|t| t.get(*n).is_some())
                    .unwrap_or(false)
            })
            .collect();
        for (n, _, _) in cases {
            let _ = crate::ported::params::unsetparam(n);
        }
        if let Err(p) = inner {
            std::panic::resume_unwind(p);
        }
        assert!(
            survivors.is_empty(),
            "{survivors:?} outlived endparamscope, i.e. pm->level was never \
             stamped and every port that declares a name this way still leaks it"
        );
    }

    /// compinit sh:523 — the scan reads `$fpath`, not the `$FPATH` the
    /// process happened to inherit.
    ///
    /// Regression: `builtin_compinit` scanned `ShellExecutor::fpath`,
    /// which is env-seeded at startup and never resynced, so
    /// `fpath=( … ); compinit` scanned the STARTUP list. With no `FPATH`
    /// exported that list is empty, the worker returned zero completers,
    /// and the empty result was written over the completion cache —
    /// `$_comps` empty, every command resolved to `-default-`.
    #[test]
    fn compinit_scans_the_live_fpath_array_not_the_startup_env() {
        use std::path::PathBuf;
        let _g = crate::test_util::global_state_lock();
        let env_seeded = vec![PathBuf::from("/from/FPATH/env")];

        crate::ported::params::setaparam(
            "fpath",
            vec!["/live/one".to_string(), "/live/two".to_string()],
        );
        assert_eq!(
            compinit_scan_dirs(&env_seeded),
            vec![PathBuf::from("/live/one"), PathBuf::from("/live/two")],
            "sh:523 scans $fpath"
        );

        // Unset / empty array — keep the env-derived list rather than
        // scanning nothing.
        crate::ported::params::setaparam("fpath", Vec::new());
        assert_eq!(compinit_scan_dirs(&env_seeded), env_seeded);
        crate::ported::params::unsetparam("fpath");
        assert_eq!(compinit_scan_dirs(&env_seeded), env_seeded);
    }

    /// `mark_readonly` is the `-r` of `local -ar` (sh:52) and must not
    /// escape the function scope: stamped at `locallevel == 0` the bit
    /// would pin the caller's global read-only forever, and the next
    /// completion's own assignment would fail with "read-only variable".
    #[test]
    fn mark_readonly_is_scoped_to_a_function() {
        use crate::ported::modules::parameter::paramtypestr;
        let _g = crate::test_util::global_state_lock();
        // `mark_readonly` keys off `locallevel` (shared.rs:216), the
        // process-wide `AtomicI32` port of `Src/params.c:54`. Nothing
        // unwinds it when a test panics out of a `doshfunc`-shaped port
        // (`_wanted_impl`'s inc/dec pair, `FnScope`, `LocalScope`), so the
        // "no scope — no readonly bit" leg below only holds from a pinned
        // 0, which is also the value a real shell starts at. Without this
        // the assertion passed alone and failed inside a full run.
        crate::ported::params::locallevel.store(0, std::sync::atomic::Ordering::Relaxed);
        let type_of = |n: &str| {
            crate::ported::params::paramtab()
                .read()
                .ok()
                .and_then(|t| t.get(n).map(|pm| paramtypestr(pm)))
                .unwrap_or_default()
        };

        crate::ported::params::setaparam("_ro_probe", vec!["a".to_string()]);
        mark_readonly(&["_ro_probe"]);
        assert_eq!(type_of("_ro_probe"), "array", "no scope — no readonly bit");

        crate::ported::utils::inc_locallevel();
        declare_locals(&["_ro_probe"], PM_ARRAY);
        crate::ported::params::setaparam("_ro_probe", vec!["b".to_string()]);
        mark_readonly(&["_ro_probe"]);
        assert_eq!(type_of("_ro_probe"), "array-local-readonly");
        crate::ported::params::endparamscope();
        assert_eq!(
            type_of("_ro_probe"),
            "array",
            "endparamscope must unwind the readonly shadow"
        );
        crate::ported::params::unsetparam("_ro_probe");
    }

    // glob_match coverage migrated from `compsys/base.rs` when the
    // local glob_match helper there was removed in favor of this
    // single shared implementation.

    #[test]
    fn test_glob_match_simple() {
        assert!(glob_match("*.txt", "file.txt"));
        assert!(glob_match("*.txt", ".txt"));
        assert!(!glob_match("*.txt", "file.rs"));
    }

    #[test]
    fn test_glob_match_question() {
        assert!(glob_match("file?.txt", "file1.txt"));
        assert!(glob_match("file?.txt", "fileX.txt"));
        assert!(!glob_match("file?.txt", "file.txt"));
        assert!(!glob_match("file?.txt", "file12.txt"));
    }

    #[test]
    fn test_glob_match_star_middle() {
        assert!(glob_match("foo*bar", "foobar"));
        assert!(glob_match("foo*bar", "foo123bar"));
        assert!(glob_match("foo*bar", "fooXYZbar"));
        assert!(!glob_match("foo*bar", "foobaz"));
    }

    #[test]
    fn test_glob_match_multiple_stars() {
        assert!(glob_match("*foo*", "foo"));
        assert!(glob_match("*foo*", "afoo"));
        assert!(glob_match("*foo*", "foob"));
        assert!(glob_match("*foo*", "afoob"));
        assert!(!glob_match("*foo*", "bar"));
    }

    #[test]
    fn test_glob_match_exact() {
        assert!(glob_match("exact", "exact"));
        assert!(!glob_match("exact", "exacty"));
        assert!(!glob_match("exact", "xact"));
    }
}

/// Call another compsys completer BY NAME, so `$fpath` arbitration still runs.
///
/// zshrs-original — C has no port tree to arbitrate against. Every upstream
/// completer reaches its helpers as a bare command word (`_files "$@"`), which
/// goes through the normal function lookup, so a user's own `_files` earlier in
/// `$fpath` wins. A Rust port that calls its sibling port as a plain Rust fn
/// skips that lookup entirely: `crate::ported::exec::dispatch_function_call` is
/// the only path that consults `compsys::router::try_rust_dispatch` and its
/// `has_fpath_override` gate, so the user's file is silently dead.
///
/// This is not hypothetical. `_command_names` had the same defect (fixed in
/// b8e714f7be) and `_parameters` had it in the `-brace-parameter-` /
/// `-subscript-` contexts, which is why `echo ${<TAB>` offered zshrs's own
/// parameter list instead of the user's. On this host `_files` is overridden at
/// `~/.zpwr/autoload/comp_utils/_files` (fpath position 18, ahead of the stock
/// tree at 24) and ten ports call it directly.
///
/// `fallback` runs only when no shell function and no registered port claims
/// the name — i.e. in unit tests with no executor installed. It is a DEGRADED
/// stand-in for the `doshfunc` frame the dispatch path opens: no `FUNCSTACK`
/// entry, no param scope, no `locallevel` bump. A caller whose sh semantics
/// depend on the callee's scope depth — anything driving `comptags`, which is
/// indexed by `locallevel` (`Src/Zle/computil.c:3782` "Array of tag-set
/// infos. Index is the locallevel", `:3873` `level = locallevel -
/// (args[0][2] ? 1 : 0)`) — must supply the missing piece inside its own
/// `fallback` closure rather than assume this helper does it.
///
/// # Naming convention for ports
///
/// A port with a dispatching entry point splits in two, and the names are
/// chosen so that the OBVIOUS call is the CORRECT one:
///
/// * `_NAME` — the dispatching wrapper, one line: `call_compfn("_NAME",
///   args, || _NAME_impl(args))`. This is what every sibling port calls, and
///   it matches the zsh function name character for character.
/// * `_NAME_impl` — the raw body. Two callers, both of which must not
///   re-enter dispatch: the wrapper's own `fallback` above, and the
///   `compsys::router` arm for `"_NAME"`. **The router arm MUST name
///   `_NAME_impl`.** Pointing it at `_NAME` makes dispatch call the wrapper,
///   which calls dispatch, forever.
///
/// Anything else that names `_NAME_impl` is asserting it genuinely needs no
/// `doshfunc` frame — sh `continue` expressed as recursion
/// (`_next_label.rs`), or a callee whose `comptags` level the caller manages
/// by hand (`_message.rs`, `_wanted.rs`). Those sites carry a comment saying
/// why.
pub fn call_compfn(name: &str, args: &[String], fallback: impl FnOnce() -> i32) -> i32 {
    crate::ported::exec::dispatch_function_call(name, args).unwrap_or_else(fallback)
}

// =====================================================================
// `scriptname` for the duration of a port call.
// =====================================================================
//
// `doshfunc` sets `scriptname` to the function's own name on entry
// (`Src/exec.c:5963` — `scriptname = dupstring(name);`) and restores the
// caller's on exit (`Src/exec.c:6124` — `scriptname = funcsave->scriptname;`).
// That is what every diagnostic reads: `zwarning` prints it ahead of the
// builtin name (`Src/utils.c:147-155`), so an error raised by a builtin
// inside `_tags` reads `_tags:comptags:36: ...`.
//
// The Rust ports reach that same builtin without a `doshfunc` frame. Ports
// call each other as plain Rust calls — `_describe` invokes `_tags` directly
// (`Base/Utility/_describe.rs`) — and only
// `crate::ported::exec::dispatch_function_call` goes through `doshfunc`. So
// `scriptname` kept whatever shell function was last entered and every
// diagnostic named the wrong function:
//
//     zsh    _tags:comptags:36: can only be called from completion function
//     zshrs  _describe:comptags: can only be called from completion function
//
// `FnScope::enter` is the `scriptname` half of that prologue/epilogue, applied
// at the entry of each port so a port called either way reports identically.
//
// `FnScope` also carries the `lineno` half. In C the second field of the
// diagnostic prefix is the GLOBAL `lineno`, printed by `zerrmsg`
// (`Src/utils.c:301-305` — `if ((unset(SHINSTDIN) || locallevel) && lineno)
// fprintf(file, "%lld: ", lineno);`), and it is maintained by the wordcode
// line markers as each statement of the function body executes
// (`Src/exec.c:1356` — `lineno = code - 1;`, `Src/exec.c:2057` —
// `lineno = WC_PIPE_LINENO(pcode) - 1;`). A Rust port has no wordcode, so
// nothing advances that counter and the field came out empty:
//
//     zsh    _describe:compdescribe:129: no parsed state
//     zshrs  _describe:compdescribe: no parsed state
//
// `execlist` saves `lineno` on entry to a body and restores it on exit
// (`Src/exec.c:1429` — `oldlineno = lineno;`, `Src/exec.c:1696` —
// `lineno = oldlineno;`), which is what makes a nested call leave the
// caller's line intact. `FnScope` reproduces that save/restore, and
// [`set_sh_lineno`] is what a port calls to stand in for the line marker.
//
// Entry deliberately publishes 0 ("unknown"), not the caller's line: 0 is the
// value `zerrmsg` treats as "no line to print", so a statement that has not
// been annotated yet keeps today's behaviour (field absent) instead of
// inheriting a number belonging to a different file. A wrong line number is
// worse than a missing one — it points the reader at the wrong function.

/// RAII guard publishing `scriptname` and `lineno` for the body of a Rust
/// compsys port, mirroring `doshfunc`'s `scriptname` save/set/restore and
/// `execlist`'s `lineno` save/restore.
pub struct FnScope {
    saved: Option<String>,
    saved_lineno: u64,
}

impl FnScope {
    /// `scriptname = dupstring(name)` (`Src/exec.c:5963`) plus
    /// `oldlineno = lineno` (`Src/exec.c:1429`), remembering the caller's
    /// values for [`Drop`].
    pub fn enter(name: &str) -> Self {
        let saved = crate::ported::utils::scriptname_get();
        crate::ported::utils::set_scriptname(Some(name.to_string()));
        let saved_lineno = crate::ported::lex::lineno();
        // No wordcode line marker has run for this body yet, and the caller's
        // line belongs to a different file — publish "unknown" so `zerrmsg`
        // omits the field (`Src/utils.c:301` — `&& lineno`).
        crate::ported::lex::set_lineno(0);
        FnScope {
            saved,
            saved_lineno,
        }
    }
}

impl Drop for FnScope {
    /// `scriptname = funcsave->scriptname` (`Src/exec.c:6124`) and
    /// `lineno = oldlineno` (`Src/exec.c:1696`).
    fn drop(&mut self) {
        crate::ported::utils::set_scriptname(self.saved.take());
        crate::ported::lex::set_lineno(self.saved_lineno);
    }
}

/// Publish the upstream shell-source line of the statement a port is about to
/// run, standing in for the wordcode line marker C executes ahead of every
/// statement (`Src/exec.c:2057` — `lineno = WC_PIPE_LINENO(pcode) - 1;`).
///
/// Diagnostics only originate at builtin call sites, so a port only needs this
/// immediately before invoking a builtin that can call `zwarnnam`; the value is
/// then read by `zwarning`/`zerrmsg` (`src/ported/utils.rs:191`).
/// [`FnScope`] restores the caller's line when the port returns.
///
/// `line` MUST be read off the upstream `Completion/**` file the port was
/// translated from. Never estimate it — the `// sh:NN` comments in the ports
/// predate later upstream edits and have drifted (`_describe`'s
/// `compdescribe -I` was annotated `sh:118-121` but lives at line 122 of both
/// zsh 5.9.2 and master).
///
/// `scripts/check_sh_lineno.py` diffs every `sh:NN` annotation against the
/// upstream file and reports the ones whose cited line does not carry the
/// quoted code; run it before trusting an annotation as a `line` argument.
/// An annotation it reports as `unverified`, `suspect` or `out-of-range` has
/// NOT been proven and must not be passed here.
pub fn set_sh_lineno(line: u64) {
    crate::ported::lex::set_lineno(line);
}

/// `eval "$comp"` — the way every compsys dispatcher invokes the completer
/// named by `$_comps` / `$_patcomps` (`_dispatch` sh:31/63/76/87,
/// `_normal` sh:32).
///
/// Upstream never CALLS the completer by name; it `eval`s the registered
/// value as shell text. Two things follow, and a port needs both:
///
///   * the value can carry arguments (`compdef '_files -/' mycmd` stores
///     `_files -/`), which a by-name dispatch cannot express; and
///   * `eval` pushes an `FS_EVAL` funcstack frame named `(eval)`
///     (`Src/builtin.c:6164-6199`), so every completer invoked this way runs
///     one frame deeper than its caller.
///
/// The frame is not cosmetic. Completion code reads `$#funcstack` to decide
/// nesting depth — `_all_labels`/`_alternative` compare it against
/// `_tags_level` — so a missing frame silently changes completion behaviour.
/// A port calling `dispatch_function_call(&comp, &[])` pushes only the
/// completer's own `FS_FUNC` frame; `$funcstack` then reads
/// `_mytest _dispatch _normal …` where zsh reports
/// `_mytest (eval) _dispatch _normal …`.
///
/// `line` is the upstream line the `eval` sits on; publishing it via
/// [`set_sh_lineno`] is what makes `$functrace` read `_dispatch:63` instead
/// of `_dispatch:0` (the caller's line is recorded at push time by `doshfunc`
/// c:6013 / `EvalFuncstackFrame::push` c:6169).
///
/// The body mirrors `static int eval(char **argv)` (`Src/builtin.c:6151`)
/// with `argv == { comp, NULL }`; the funcstack half is the shared canonical
/// port [`crate::ported::exec::EvalFuncstackFrame`] (c:6164-6199), the same
/// one the live `eval` builtin uses, so both entry points build an identical
/// frame.
pub fn eval_comp(comp: &str, line: u64) -> i32 {
    set_sh_lineno(line);
    let oscriptname = crate::ported::utils::scriptname_get(); // c:6154
    let fstack = crate::ported::exec::EvalFuncstackFrame::push(); // c:6164-6199
    if fstack.pushed() {
        // c:6165 — `scriptname = "(eval)";` (inside the `!ineval` arm).
        crate::ported::utils::set_scriptname(Some("(eval)".to_string()));
    }
    // c:6209 — `execode(prog, 1, 0, "eval");` APPENDS its context argument to
    // `zsh_eval_context` for the duration of the body (Src/exec.c:1245-1266).
    //
    // That push is DELIBERATELY NOT made here. `docs/COMPLETION_DISPATCH.md`
    // "Divergence C" records the decision that compsys Rust ports do not
    // synthesize `$zsh_eval_context` frames, and
    // tests/zsh_eval_context_frames.rs::compsys_ports_synthesize_no_eval_context_frames
    // pins it by scanning this tree for that constructor call. (The scan is a
    // plain substring match, so naming the call verbatim here — even in prose —
    // trips it; hence the circumlocution.)
    //
    // A push was added here in 9e55378587 and broke that test. It is left out
    // rather than re-added, and the test is left alone, because the decision is
    // documented and the test is its enforcement — not because the case is
    // clear-cut. It is not: Divergence C reasons that the Rust chain "never
    // evals", whereas this function genuinely does parse and execute a string
    // below, so a frame here would arguably be truthful rather than fabricated.
    // Resolving that tension is a design call for the maintainer; silently
    // overriding a pinned decision from inside a bug fix is not.
    //
    // The funcstack half above is separate and IS pushed: it is a real frame
    // for a call that really happens, and no invariant forbids it.
    //
    // c:6203-6216 — `prog = parse_string(...); … execode(prog, …)`; a NULL
    // prog (parse failure) is `lastval = 1` at c:6215.
    let mut lastval = crate::ported::exec::execute_script(comp).unwrap_or(1);
    // c:6211-6212 — `if (errflag && !lastval) lastval = errflag;`
    {
        let ef = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
        if ef != 0 && lastval == 0 {
            lastval = ef;
        }
    }
    drop(fstack); // c:6218-6219 `if (fpushed) funcstack = funcstack->prev;`
                  // c:6221 — `errflag &= ~ERRFLAG_ERROR;`
                  //
                  // `eval` swallows the error bit on the way out, UNCONDITIONALLY, and that
                  // is load-bearing for completion: it is what lets an error deep inside a
                  // completer TRUNCATE the work while KEEPING the matches already banked.
                  // `_CC` ends in `_files -g "*(-.):t:source files" -g "*(-/):t:directories"`,
                  // which brace-expands to four sdefs; the third is a genuine bad pattern
                  // (`/` ends a segment at EVERY paren depth — c:Src/pattern.c:949-952 — so
                  // the group is unterminated and c:Src/pattern.c:913-914 rejects it).
                  // `zerr` raises ERRFLAG_ERROR (c:Src/utils.c:184); that aborts
                  // `_path_files`, `_files` (so the FOURTH sdef, `*(-/)` = every directory,
                  // never runs), `_arguments` and `_CC` — and then THIS clear runs at the
                  // `eval "$comp"` in sh:Completion/Base/Core/_dispatch:63, so
                  // `makecomplist`'s `(nmatches || nmessages) && !errflag`
                  // (c:Src/Zle/compcore.c:1031) still sees errflag == 0 and keeps the one
                  // match added before the error.  Without it the completion is discarded
                  // and `CC <TAB>` produces nothing at all.
                  //
                  // Measured on an instrumented zsh 5.9.999.3 driving `CC <TAB>`:
                  //   ZDBG SET   utils.c:184 zerr fmt=<bad pattern: %s> errflag=1
                  //   ZDBG FUNC< _path_files errflag=1 ret=1
                  //   ZDBG FUNC< _files      errflag=1 ret=1
                  //   ZDBG FUNC< _arguments  errflag=1 ret=1
                  //   ZDBG FUNC< _CC         errflag=1 ret=1
                  //   ZDBG CLR   builtin.c:6213 before=1 stmt=<errflag &= ~ERRFLAG_ERROR;>
                  //   ZDBG FUNC< _dispatch   errflag=0 ret=1
                  //   ZDBG CHECK compcore.c:1031 nmatches=1 nmessages=0 errflag=0
                  // Of the 87 errflag clear sites in the C source, that is the ONLY one in
                  // the whole session that observes a set ERRFLAG_ERROR.
    crate::ported::utils::errflag.fetch_and(
        !crate::ported::zsh_h::ERRFLAG_ERROR,
        std::sync::atomic::Ordering::Relaxed,
    );
    crate::ported::utils::set_scriptname(oscriptname); // c:6222
    lastval // c:6225
}

/// Run an already-expanded action word list as a COMMAND, the way the shell
/// does at `_arguments` sh:455 / sh:465 and `_all_labels` sh:35 / sh:39.
///
/// `cmd` is the command word (`$action[1]`), `argv` the rest of the words the
/// shell would have passed, and `line` the upstream line the command sits on
/// — it is published through [`set_sh_lineno`] so any diagnostic this raises
/// carries the same `_arguments:465:` prefix zsh prints.
///
/// **Why a helper rather than a bare
/// [`crate::ported::exec::dispatch_function_call`]:** that entry resolves
/// SHELL FUNCTIONS and the native ports standing in for them, and nothing
/// else — `vm_helper.rs:4706` ends in `functions_compiled.get(name).cloned()?`,
/// so a builtin, an executable on `$PATH`, and a name that exists nowhere all
/// come back `None`. Call sites turned that `None` into a plain non-zero
/// status, which means a command word the shell would have DIAGNOSED
/// disappeared without a byte of output. Measured on
/// `~/.zinit/plugins/MenkeTechnologies---zsh-more-completions/more_src6/_tor-resolve`,
/// whose rest spec is
///
/// ```text
/// '*:hostname or IP and SOCKS host[:port]:'
/// ```
///
/// — a MESSAGE carrying an unescaped `:`, so `parse_caarg` (c:1137-1138,
/// `mult == 0`) takes everything after the first colon as the ACTION and
/// `_arguments` runs `port]:` as a command. zsh reports
/// `_arguments:465: command not found: port]:` and zshrs printed nothing at
/// all, which is the "one-sided silence" shape that reads as a hang.
///
/// The three arms mirror what `execcmd` does with a command word: a shell
/// function (or port) runs; a name that IS a builtin or IS on `$PATH` is real
/// but has no execution route from here, so it keeps the pre-existing
/// "action did not succeed" answer rather than having one invented for it;
/// and only a name that resolves to nothing reaches
/// `Src/exec.c:903`'s diagnostic.
///
/// `zwarn`, NOT `zerr`: c:903 runs in the FORKED child of `execcmd`, which
/// `_exit(127)`s at c:908, so the parent shell's `errflag` is never raised.
/// `zerr` here runs in the live shell and would set `ERRFLAG_ERROR`
/// (`src/ported/utils.rs:236`), abandoning the rest of the completion.
/// The same fork has a second consequence — the child already cleared
/// `zleactive`, so the diagnostic must not repaint the editor — which
/// the `SubshStateGuard` at the emission site carries; see the comment
/// there.
///
/// This is the extraction of `_all_labels`' private `dispatch_action` tail,
/// which had the same three arms; that port now calls this one, so the
/// behaviour lives in a single place instead of being re-derived per caller.
pub fn dispatch_action_command(cmd: &str, argv: &[String], line: u64) -> i32 {
    // The line has to be published BEFORE anything can diagnose: `lineno` is
    // what `zerrmsg` prints after the function name (`Src/utils.c:301-305`),
    // and `FnScope` zeroed it on entry to the port's body.
    set_sh_lineno(line);

    // `compadd` is a BUILTIN, so `dispatch_function_call` finds no shell
    // function for it and the action would add nothing. Route it to the real
    // builtin in `src/ported/zle/complete`.
    if cmd == "compadd" {
        let ops = crate::ported::zsh_h::options {
            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
            args: Vec::new(),
            argscount: 0,
            argsalloc: 0,
        };
        return crate::ported::zle::complete::bin_compadd("compadd", argv, &ops, 0);
    }

    if let Some(rc) = crate::ported::exec::dispatch_function_call(cmd, argv) {
        return rc;
    }

    // Neither a shell function nor a registered port. zsh looks for a builtin
    // and then for an executable on `$PATH`; only when both miss does it
    // report the command as not found.
    if crate::ported::builtin::createbuiltintable().contains_key(cmd)
        || crate::ported::exec::findcmd(cmd, 0, 0).is_some()
    {
        return 1;
    }

    // c:Src/exec.c:903 — `zerr("command not found: %s", arg0);`
    //
    // The FORKED CHILD is not just about `errflag`. Before `execute()`
    // ever reaches c:903 the child has run `entersubsh()`, whose last
    // two scalar deltas are `opts[USEZLE] = 0; zleactive = 0;`
    // (Src/exec.c:1247-1248). `zwarning` opens with
    // `if (isatty(2)) zleentry(ZLE_CMD_TRASH);` (Src/utils.c:144-145)
    // and `trashzle` is gated on `if (zleactive && !trashedzle)`
    // (Src/Zle/zle_main.c:2071), so in C that hook does NOTHING here:
    // no `zrefresh`, no `moveto(nlnct, 0)`, no `resetneeded = 1`. The
    // diagnostic lands wherever ZLE left the cursor — appended to the
    // command line's own row — and the editor display is not repainted.
    //
    // zshrs runs this in the live shell with no fork, so without the
    // guard `zleactive` is still 1, `trashzle` fires, and its
    // `moveto(nlnct, 0)` writes `\r` + `\n` before the text while
    // `resetneeded` makes the following `zrefresh` repaint the prompt
    // after it. Measured against zsh on `_alternative 'x:x: nosuchcmd'`:
    //   zsh  : row 0 `$ true _alternative:63: command not found: …`
    //   zshrs: row 0 `$ true`, row 1 the diagnostic, row 2 the prompt again
    // — the same text, three rows instead of one.
    //
    // `SubshStateGuard` (exec.rs) is the ported stand-in for exactly
    // those `entersubsh` deltas that are correct without a fork, and it
    // restores them on drop.
    let _subsh = crate::ported::exec::SubshStateGuard::enter(); // c:1247-1248
    crate::ported::utils::zwarn(&format!("command not found: {}", cmd)); // c:903
    127 // c:908 — `_exit((eno == EACCES || eno == ENOEXEC) ? 126 : 127)`
}

// =====================================================================
// `$( <builtin> )` — capturing a builtin's stdout without a subshell.
// =====================================================================

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// The `$( … )` around a BUILTIN, for the ports whose upstream source
/// reads a builtin's stdout: `_limits` sh:5 `$(limit)`, `_parameters`
/// sh:34 `$( typeset -m … )`, `_correct_filename` sh:60
/// `$(whence -wm …)`, `_user_math_func` sh:6 `$(functions -M)`.
///
/// C has no counterpart because C never needs one — a shell has no way
/// to reach a builtin's stdout EXCEPT a command substitution, so
/// upstream pays for a fork and a full parse to read a table the same
/// process already holds. `exec::run_command_substitution` reproduces
/// that faithfully, including the deep clone of all shell state that
/// every real `$( … )` performs; on a completer that runs per keystroke
/// the clone is the entire cost. This runs the builtin in-process with
/// fd 1 pointed at a scratch file instead, which reaches the identical
/// bytes.
///
/// It exists so there is ONE of these. Two hand-rolled copies had
/// already appeared (`_parameters`, then `_correct_filename` explicitly
/// copying it), and each got to decide independently whether to restore
/// fd 1 on the failure path, whether to flush first, and whether to
/// delete the scratch file.
///
/// A temp FILE, not a pipe: these listings are unbounded (`typeset -m`
/// over `$PATH`/`$LS_COLORS`, `whence -m` over a large `$PATH`) and a
/// pipe would deadlock the moment the output outgrew its 64K buffer
/// with nobody draining the read end.
///
/// `discard_stderr` is the `2>/dev/null` some of those call sites write.
///
/// Returns what the builtin printed, or the empty string if the scratch
/// file cannot be made or fd 1 cannot be redirected — in which case the
/// builtin is NOT run, so a failure here adds nothing wrong, it only
/// leaves the caller with no candidates.
pub fn capture_builtin_stdout(discard_stderr: bool, run: impl FnOnce()) -> String {
    let (fd, path) = match crate::ported::utils::gettempfile(None) {
        Some(t) => t,
        None => return String::new(),
    };
    // Flush FIRST: `println!` writes through a `LineWriter` over fd 1, and
    // anything still buffered from before the redirect would otherwise land
    // in the capture instead of on the terminal.
    let _ = std::io::Write::flush(&mut std::io::stdout());
    let saved_out = unsafe { libc::dup(1) };
    let saved_err = if discard_stderr {
        unsafe { libc::dup(2) }
    } else {
        -1
    };
    let devnull = if discard_stderr {
        unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY) }
    } else {
        -1
    };
    let redirected = saved_out >= 0 && unsafe { libc::dup2(fd, 1) } >= 0;
    if redirected {
        let err_redirected =
            devnull >= 0 && saved_err >= 0 && unsafe { libc::dup2(devnull, 2) } >= 0;
        run();
        let _ = std::io::Write::flush(&mut std::io::stdout());
        unsafe {
            libc::dup2(saved_out, 1);
        }
        if err_redirected {
            unsafe {
                libc::dup2(saved_err, 2);
            }
        }
    }
    for f in [saved_out, saved_err, devnull, fd] {
        if f >= 0 {
            unsafe {
                libc::close(f);
            }
        }
    }
    let text = if redirected {
        std::fs::read_to_string(&path).unwrap_or_default()
    } else {
        String::new()
    };
    let _ = std::fs::remove_file(&path);
    text
}

#[cfg(test)]
mod capture_builtin_stdout_tests {
    use super::*;

    /// RUST-ONLY test scaffold: write straight to the fd. A ported builtin
    /// reaches fd 1 through `println!`, but libtest replaces the `print!`
    /// macros' sink (`std::io::set_output_capture`), so a `println!` here
    /// would never reach the redirect and the test would measure libtest
    /// rather than this function.
    fn write_fd(fd: i32, s: &str) {
        unsafe {
            libc::write(fd, s.as_ptr() as *const libc::c_void, s.len());
        }
    }

    /// The captured text is what the builtin printed, and fd 1 is the
    /// caller's again afterwards.
    #[test]
    fn captures_stdout_and_restores_fd1() {
        let _g = crate::test_util::global_state_lock();

        let text = capture_builtin_stdout(false, || {
            write_fd(1, "one\ntwo\n");
        });

        assert_eq!(text, "one\ntwo\n");
        // fd 1 still writes somewhere valid — a leaked redirect shows up
        // as an EBADF here, and as vanished output for the rest of the
        // process.
        assert!(
            unsafe { libc::fcntl(1, libc::F_GETFD) } >= 0,
            "fd 1 was not restored"
        );
    }

    /// `2>/dev/null` on the call site must keep stderr OUT of the capture
    /// and must not leave fd 2 pointing at `/dev/null` afterwards.
    #[test]
    fn discard_stderr_leaves_fd2_usable() {
        let _g = crate::test_util::global_state_lock();
        let text = capture_builtin_stdout(true, || {
            write_fd(1, "out\n");
            write_fd(2, "this must not be captured\n");
        });
        assert_eq!(text, "out\n");
        assert!(
            unsafe { libc::fcntl(2, libc::F_GETFD) } >= 0,
            "fd 2 was not restored"
        );
    }
}

#[cfg(test)]
mod lineno_scope_tests {
    use super::*;
    use crate::ported::lex::{lineno, set_lineno};

    /// `zerrmsg` prints the line only when it is non-zero
    /// (`Src/utils.c:301` — `&& lineno`), so a port body must start at 0:
    /// an un-annotated statement has to report NO line rather than inherit
    /// the caller's, which belongs to a different file.
    #[test]
    fn fn_scope_zeroes_lineno_for_the_port_body() {
        let _g = crate::test_util::global_state_lock();
        set_lineno(218); // caller mid-body, e.g. _main_complete sh:218
        {
            let _s = FnScope::enter("_describe");
            assert_eq!(lineno(), 0, "port body must start with no known line");
        }
    }

    /// `execlist` restores the caller's line when a body finishes
    /// (`Src/exec.c:1429` / `Src/exec.c:1696`), which is what keeps a
    /// nested call from renumbering its caller's diagnostics.
    #[test]
    fn fn_scope_restores_the_callers_lineno_on_exit() {
        let _g = crate::test_util::global_state_lock();
        set_lineno(218);
        {
            let _s = FnScope::enter("_describe");
            set_sh_lineno(129);
            assert_eq!(lineno(), 129);
        }
        assert_eq!(lineno(), 218, "caller's line must survive the port call");
    }

    /// Nested ports each restore their own caller, so `_describe` calling
    /// `_tags` leaves `_describe`'s line intact for the statement after it.
    #[test]
    fn nested_fn_scopes_unwind_to_the_right_line() {
        let _g = crate::test_util::global_state_lock();
        set_lineno(0);
        let outer = FnScope::enter("_describe");
        set_sh_lineno(122);
        {
            let _inner = FnScope::enter("_tags");
            assert_eq!(lineno(), 0);
            set_sh_lineno(36); // _tags sh:36 — comptags "-i$prev" …
            assert_eq!(lineno(), 36);
        }
        assert_eq!(lineno(), 122, "_describe's line must survive _tags");
        drop(outer);
        assert_eq!(lineno(), 0);
    }
}

#[cfg(test)]
mod diagnostic_framing_tests {
    use std::sync::atomic::Ordering;

    /// The `command not found` diagnostic must NOT trash the line editor.
    ///
    /// `Src/exec.c:903` is reached only inside the fork `execcmd` makes for
    /// an external command, and that child has already run `entersubsh`,
    /// whose last two scalar deltas are `opts[USEZLE] = 0; zleactive = 0;`
    /// (`Src/exec.c:1247-1248`). `zwarning` opens with
    /// `if (isatty(2)) zleentry(ZLE_CMD_TRASH);` (`Src/utils.c:144-145`)
    /// and `trashzle` runs its body only `if (zleactive && !trashedzle)`
    /// (`Src/Zle/zle_main.c:2071`) — so with `zleactive` cleared the hook
    /// is a no-op and the diagnostic lands on the command line's own row
    /// with no repaint after it.
    ///
    /// The two flags asserted here are the ones `trashzle` sets on its way
    /// through: `trashedzle = 1` (c:2079) and `resetneeded = 1` (c:2091),
    /// the second of which is what makes the NEXT `zrefresh` repaint the
    /// prompt below the diagnostic. Either being raised is the three-row
    /// display zshrs used to produce where zsh produces one.
    ///
    /// `fd 2` is pointed at a pty for the duration: `isatty(2)` is false
    /// under `cargo test`, and with it false `zwarning` never reaches the
    /// hook at all — the case would pass without measuring anything.
    #[test]
    fn the_not_found_diagnostic_does_not_trash_the_line_editor() {
        use crate::ported::builtins::sched::zleactive;
        use crate::ported::init::zle_load_state;
        use crate::ported::zle::zle_refresh::{RESETNEEDED, TRASHEDZLE};

        let _g = crate::test_util::global_state_lock();

        let mut master: libc::c_int = 0;
        let mut slave: libc::c_int = 0;
        let rc = unsafe {
            libc::openpty(
                &mut master,
                &mut slave,
                std::ptr::null_mut(),
                std::ptr::null_mut::<libc::termios>(),
                std::ptr::null_mut::<libc::winsize>(),
            )
        };
        assert_eq!(rc, 0, "openpty failed; the probe needs a terminal on fd 2");
        let saved_stderr = unsafe { libc::dup(2) };
        assert!(saved_stderr >= 0, "dup(2) failed");
        assert!(
            unsafe { libc::dup2(slave, 2) } >= 0,
            "dup2 onto fd 2 failed"
        );
        assert_eq!(
            unsafe { libc::isatty(2) },
            1,
            "fd 2 must be a terminal or Src/utils.c:144 skips the hook"
        );

        let saved = (
            zleactive.load(Ordering::Relaxed),
            zle_load_state.load(Ordering::SeqCst),
            TRASHEDZLE.load(Ordering::Relaxed),
            RESETNEEDED.load(Ordering::Relaxed),
        );
        // `zle_load_state == 1` is "the module is loaded", the state in
        // which `zleentry` forwards to `zle_main_entry` (Src/init.c:1777)
        // rather than to the no-ZLE fallback. An interactive shell running
        // a completer is always in it.
        zleactive.store(1, Ordering::Relaxed);
        zle_load_state.store(1, Ordering::SeqCst);
        TRASHEDZLE.store(0, Ordering::Relaxed);
        RESETNEEDED.store(0, Ordering::Relaxed);
        crate::ported::utils::errflag.store(0, Ordering::Relaxed);

        let status = super::dispatch_action_command("nosuchcmd_zz_framing_probe", &[], 63);

        let trashed = TRASHEDZLE.load(Ordering::Relaxed);
        let reset = RESETNEEDED.load(Ordering::Relaxed);
        let still_active = zleactive.load(Ordering::Relaxed);

        zleactive.store(saved.0, Ordering::Relaxed);
        zle_load_state.store(saved.1, Ordering::SeqCst);
        TRASHEDZLE.store(saved.2, Ordering::Relaxed);
        RESETNEEDED.store(saved.3, Ordering::Relaxed);
        crate::ported::utils::errflag.store(0, Ordering::Relaxed);
        unsafe {
            libc::dup2(saved_stderr, 2);
            libc::close(saved_stderr);
            libc::close(slave);
            libc::close(master);
        }

        assert_eq!(status, 127, "c:908 — a name that resolves nowhere is 127");
        assert_eq!(
            trashed, 0,
            "trashzle ran: the diagnostic moved the cursor off the command \
             line's row (Src/Zle/zle_main.c:2071 is false in C's forked child)"
        );
        assert_eq!(
            reset, 0,
            "resetneeded was raised: the next zrefresh repaints the prompt \
             BELOW the diagnostic, which zsh never does here"
        );
        assert_eq!(
            still_active, 1,
            "the entersubsh stand-in must restore zleactive when it drops"
        );
    }
}

#[cfg(test)]
mod zstyle_bool_tests {
    use super::{empty_ops, zstyle_T, zstyle_t};

    const CTX: &str = ":completion:zstyle-bool-probe:zstyle-bool-probe:";

    fn set_style(value: &str) {
        crate::ported::modules::zutil::bin_zstyle(
            "zstyle",
            &[CTX.to_string(), "boolprobe".to_string(), value.to_string()],
            &empty_ops(),
            0,
        );
    }

    fn del_style() {
        crate::ported::modules::zutil::bin_zstyle(
            "zstyle",
            &["-d".to_string(), CTX.to_string(), "boolprobe".to_string()],
            &empty_ops(),
            0,
        );
    }

    /// `Src/Modules/zutil.c:701-724` — the `-t`/`-T` arm reads the style's
    /// FIRST VALUE. `testforstyle` (c:465), which backs `zstyle -q` (c:749-756),
    /// answers only "is this style defined" and returns the same thing for
    /// `boolprobe yes` and `boolprobe 0`. Every compsys port that tested a
    /// boolean style through it therefore took the TRUE branch for a style the
    /// user had explicitly turned OFF.
    #[test]
    fn value_decides_the_exit_not_mere_definition() {
        let _g = crate::test_util::global_state_lock();
        del_style();

        // c:724 — `return (args[0][1] == 't' ? (vals ? 1 : 2) : 0);`
        assert_eq!(zstyle_t(CTX, "boolprobe"), 2, "-t, no pattern matched → 2");
        assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T, no pattern matched → 0");

        // c:719-722 — the four boolean-true spellings.
        for v in ["true", "yes", "on", "1"] {
            del_style();
            set_style(v);
            assert_eq!(zstyle_t(CTX, "boolprobe"), 0, "-t on `{v}` must be 0");
            assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T on `{v}` must be 0");
        }

        // c:719-722 — anything else is FALSE for both letters. This is the
        // whole defect class: `testforstyle` answered 0 ("defined") for each
        // of these, so the ports ran the true branch.
        // An ARBITRARY non-boolean value is FALSE for both letters too —
        // `-T` is "true unless it is set to something that is not true", NOT
        // "true unless it is set to one of the four false-y words". Five
        // private `zstyle_t_default_true` copies had spelled it
        // `!matches!(first, "no"|"false"|"off"|"0")`, which answers TRUE here;
        // `_describe`'s copy of the same-named helper answered FALSE, and the
        // disagreement between two bodies under one name is how it surfaced.
        for v in ["maybe", "2", "yes-ish", "-1"] {
            del_style();
            set_style(v);
            assert_eq!(
                zstyle_t(CTX, "boolprobe"),
                1,
                "-t on `{v}` must be 1 — only true/yes/on/1 are true"
            );
            assert_eq!(
                zstyle_T(CTX, "boolprobe"),
                1,
                "-T on `{v}` must be 1 — a set-but-not-true value is FALSE"
            );
        }

        // Set with NO values is a distinct case from unset, and both are
        // `-T` true (c:724 `vals ? 1 : 2` for -t; the -T arm returns 0).
        del_style();
        crate::ported::modules::zutil::bin_zstyle(
            "zstyle",
            &[CTX.to_string(), "boolprobe".to_string()],
            &empty_ops(),
            0,
        );
        assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T on a valueless style → 0");

        for v in ["false", "no", "off", "0"] {
            del_style();
            set_style(v);
            assert_eq!(
                zstyle_t(CTX, "boolprobe"),
                1,
                "-t on `{v}` must be 1, not 0 — the style is OFF"
            );
            assert_eq!(
                zstyle_T(CTX, "boolprobe"),
                1,
                "-T on `{v}` must be 1, not 0 — the style is OFF"
            );
            // The defect itself, pinned: the primitive the ports used
            // answers the SAME thing here as it does for `yes`. Every
            // converted site spelled its test `testforstyle(…) == 0`, so
            // every one of them ran the true branch for a style the user
            // had switched off.
            assert_eq!(
                crate::ported::modules::zutil::testforstyle(CTX, "boolprobe"),
                0,
                "`testforstyle` reports `{v}` as TRUE — it is zstyle -q's \
                 primitive (zutil.c:465/749-756) and never reads the value"
            );
        }

        del_style();
    }
}