cli.bend

cli@0.2.0.0 · 8,031 lines · 988 definitions

1# Purpose: pure command definitions, validation, and text rendering for Bend CLIs.
2# Expected output: ALL PROOFS CHECK.
3# Run: bend cli.bend --check-only
4
5import Base
6
7type Style is Data:
8  Bold{}
9  Dim{}
10  Under{}
11  Red{}
12  Green{}
13  Yellow{}
14  Blue{}
15  Magenta{}
16  Cyan{}
17
18type Segment is Data:
19  Segment{text: String, styles: List<&2, Style>}
20
21def style_code(style: Style) -> String:
22  match style:
23    case Bold{}: "1"
24    case Dim{}: "2"
25    case Under{}: "4"
26    case Red{}: "31"
27    case Green{}: "32"
28    case Yellow{}: "33"
29    case Blue{}: "34"
30    case Magenta{}: "35"
31    case Cyan{}: "36"
32
33def style_codes_tail(styles: List<&2, Style>) -> String:
34  match styles:
35    case Nil{}: ""
36    case h <> t:
37      String.append(";", String.append(style_code(h), style_codes_tail(t)))
38
39def style_codes(styles: List<&2, Style>) -> String:
40  match styles:
41    case Nil{}: ""
42    case h <> t:
43      String.append(style_code(h), style_codes_tail(t))
44
45def paint(on: Bool, styles: List<&2, Style>, text: String) -> String:
46  match on styles:
47    case False{} _:
48      text
49    case True{} Nil{}:
50      text
51    case True{} h <> t:
52      String.append("\u{1B}[",
53        String.append(style_codes(h <> t),
54          String.append("m", String.append(text, "\u{1B}[0m"))))
55
56def emit_go(segments: List<&2, Segment>, +on: Bool) -> String:
57  match segments:
58    case Nil{}:
59      ""
60    case h <> t:
61      match h:
62        case Segment{text, styles}:
63          String.append(paint(on, styles, text), emit_go(t, on))
64
65def emit(on: Bool, segments: List<&2, Segment>) -> String:
66  emit_go(segments, on)
67
68def sgr_body_char(+c: Char) -> Bool:
69  Bool.or(Char.is_digit(c), Char.is_eq(c, ';'))
70
71type StripState is Data:
72  StripPlain{}
73  StripEsc{}
74  StripBody{pending: String}
75
76def strip_go(s: String, state: StripState) -> String:
77  match s:
78    case SNil{}:
79      match state:
80        case StripPlain{}: ""
81        case StripEsc{}: "\u{1B}"
82        case StripBody{pending}: pending
83    case SCon{+h, t}:
84      match state:
85        case StripPlain{}:
86          +esc = Char.is_eq(h, '\u{1B}')
87          String.append(Bool.pick(String, esc, "", Char.show(h)),
88            strip_go(t, Bool.pick(StripState, esc, StripEsc{}, StripPlain{})))
89        case StripEsc{}:
90          +bracket = Char.is_eq(h, '[')
91          +esc = Char.is_eq(h, '\u{1B}')
92          +prefix = Bool.pick(String, bracket, "",
93            Bool.pick(String, esc, "\u{1B}",
94              String.append("\u{1B}", Char.show(h))))
95          +next = Bool.pick(StripState, bracket, StripBody{"\u{1B}["},
96            Bool.pick(StripState, esc, StripEsc{}, StripPlain{}))
97          String.append(prefix, strip_go(t, next))
98        case StripBody{+pending}:
99          +end = Char.is_eq(h, 'm')
100          +esc = Char.is_eq(h, '\u{1B}')
101          +ok = sgr_body_char(h)
102          +grown = String.append(pending, Char.show(h))
103          +prefix = Bool.pick(String, end, "",
104            Bool.pick(String, esc, pending,
105              Bool.pick(String, ok, "", grown)))
106          +next = Bool.pick(StripState, end, StripPlain{},
107            Bool.pick(StripState, esc, StripEsc{},
108              Bool.pick(StripState, ok, StripBody{grown}, StripPlain{})))
109          String.append(prefix, strip_go(t, next))
110
111def strip(s: String) -> String:
112  strip_go(s, StripPlain{})
113
114type CodeRange is Data:
115  CodeRange{lo: U32, hi: U32}
116
117def contains_range(ranges: List<&2, CodeRange>, +code: U32) -> Bool:
118  match ranges:
119    case Nil{}:
120      False{}
121    case CodeRange{lo, hi} <> tail:
122      Bool.or(Bool.and(U32.is_ge(code, lo), U32.is_le(code, hi)),
123        contains_range(tail, code))
124
125# Unicode 15.1 East Asian Wide and Fullwidth ranges, including reserved ranges.
126# Source: https://www.unicode.org/Public/15.1.0/ucd/EastAsianWidth.txt
127def unicode_is_wide(code: U32) -> Bool:
128  contains_range([
129    CodeRange{4352, 4447},
130    CodeRange{8986, 8987},
131    CodeRange{9001, 9002},
132    CodeRange{9193, 9196},
133    CodeRange{9200, 9200},
134    CodeRange{9203, 9203},
135    CodeRange{9725, 9726},
136    CodeRange{9748, 9749},
137    CodeRange{9800, 9811},
138    CodeRange{9855, 9855},
139    CodeRange{9875, 9875},
140    CodeRange{9889, 9889},
141    CodeRange{9898, 9899},
142    CodeRange{9917, 9918},
143    CodeRange{9924, 9925},
144    CodeRange{9934, 9934},
145    CodeRange{9940, 9940},
146    CodeRange{9962, 9962},
147    CodeRange{9970, 9971},
148    CodeRange{9973, 9973},
149    CodeRange{9978, 9978},
150    CodeRange{9981, 9981},
151    CodeRange{9989, 9989},
152    CodeRange{9994, 9995},
153    CodeRange{10024, 10024},
154    CodeRange{10060, 10060},
155    CodeRange{10062, 10062},
156    CodeRange{10067, 10069},
157    CodeRange{10071, 10071},
158    CodeRange{10133, 10135},
159    CodeRange{10160, 10160},
160    CodeRange{10175, 10175},
161    CodeRange{11035, 11036},
162    CodeRange{11088, 11088},
163    CodeRange{11093, 11093},
164    CodeRange{11904, 11929},
165    CodeRange{11931, 12019},
166    CodeRange{12032, 12245},
167    CodeRange{12272, 12350},
168    CodeRange{12353, 12438},
169    CodeRange{12441, 12543},
170    CodeRange{12549, 12591},
171    CodeRange{12593, 12686},
172    CodeRange{12688, 12771},
173    CodeRange{12783, 12830},
174    CodeRange{12832, 12871},
175    CodeRange{12880, 19903},
176    CodeRange{19968, 42124},
177    CodeRange{42128, 42182},
178    CodeRange{43360, 43388},
179    CodeRange{44032, 55203},
180    CodeRange{63744, 64255},
181    CodeRange{65040, 65049},
182    CodeRange{65072, 65106},
183    CodeRange{65108, 65126},
184    CodeRange{65128, 65131},
185    CodeRange{65281, 65376},
186    CodeRange{65504, 65510},
187    CodeRange{94176, 94180},
188    CodeRange{94192, 94193},
189    CodeRange{94208, 100343},
190    CodeRange{100352, 101589},
191    CodeRange{101632, 101640},
192    CodeRange{110576, 110579},
193    CodeRange{110581, 110587},
194    CodeRange{110589, 110590},
195    CodeRange{110592, 110882},
196    CodeRange{110898, 110898},
197    CodeRange{110928, 110930},
198    CodeRange{110933, 110933},
199    CodeRange{110948, 110951},
200    CodeRange{110960, 111355},
201    CodeRange{126980, 126980},
202    CodeRange{127183, 127183},
203    CodeRange{127374, 127374},
204    CodeRange{127377, 127386},
205    CodeRange{127488, 127490},
206    CodeRange{127504, 127547},
207    CodeRange{127552, 127560},
208    CodeRange{127568, 127569},
209    CodeRange{127584, 127589},
210    CodeRange{127744, 127776},
211    CodeRange{127789, 127797},
212    CodeRange{127799, 127868},
213    CodeRange{127870, 127891},
214    CodeRange{127904, 127946},
215    CodeRange{127951, 127955},
216    CodeRange{127968, 127984},
217    CodeRange{127988, 127988},
218    CodeRange{127992, 128062},
219    CodeRange{128064, 128064},
220    CodeRange{128066, 128252},
221    CodeRange{128255, 128317},
222    CodeRange{128331, 128334},
223    CodeRange{128336, 128359},
224    CodeRange{128378, 128378},
225    CodeRange{128405, 128406},
226    CodeRange{128420, 128420},
227    CodeRange{128507, 128591},
228    CodeRange{128640, 128709},
229    CodeRange{128716, 128716},
230    CodeRange{128720, 128722},
231    CodeRange{128725, 128727},
232    CodeRange{128732, 128735},
233    CodeRange{128747, 128748},
234    CodeRange{128756, 128764},
235    CodeRange{128992, 129003},
236    CodeRange{129008, 129008},
237    CodeRange{129292, 129338},
238    CodeRange{129340, 129349},
239    CodeRange{129351, 129535},
240    CodeRange{129648, 129660},
241    CodeRange{129664, 129672},
242    CodeRange{129680, 129725},
243    CodeRange{129727, 129733},
244    CodeRange{129742, 129755},
245    CodeRange{129760, 129768},
246    CodeRange{129776, 129784},
247    CodeRange{131072, 196605},
248    CodeRange{196608, 262141},
249  ], code)
250
251# Unicode 15.1 nonspacing and enclosing mark ranges (Mn and Me).
252# Source: https://www.unicode.org/Public/15.1.0/ucd/UnicodeData.txt
253def unicode_is_zero_mark(code: U32) -> Bool:
254  contains_range([
255    CodeRange{768, 879},
256    CodeRange{1155, 1161},
257    CodeRange{1425, 1469},
258    CodeRange{1471, 1471},
259    CodeRange{1473, 1474},
260    CodeRange{1476, 1477},
261    CodeRange{1479, 1479},
262    CodeRange{1552, 1562},
263    CodeRange{1611, 1631},
264    CodeRange{1648, 1648},
265    CodeRange{1750, 1756},
266    CodeRange{1759, 1764},
267    CodeRange{1767, 1768},
268    CodeRange{1770, 1773},
269    CodeRange{1809, 1809},
270    CodeRange{1840, 1866},
271    CodeRange{1958, 1968},
272    CodeRange{2027, 2035},
273    CodeRange{2045, 2045},
274    CodeRange{2070, 2073},
275    CodeRange{2075, 2083},
276    CodeRange{2085, 2087},
277    CodeRange{2089, 2093},
278    CodeRange{2137, 2139},
279    CodeRange{2200, 2207},
280    CodeRange{2250, 2273},
281    CodeRange{2275, 2306},
282    CodeRange{2362, 2362},
283    CodeRange{2364, 2364},
284    CodeRange{2369, 2376},
285    CodeRange{2381, 2381},
286    CodeRange{2385, 2391},
287    CodeRange{2402, 2403},
288    CodeRange{2433, 2433},
289    CodeRange{2492, 2492},
290    CodeRange{2497, 2500},
291    CodeRange{2509, 2509},
292    CodeRange{2530, 2531},
293    CodeRange{2558, 2558},
294    CodeRange{2561, 2562},
295    CodeRange{2620, 2620},
296    CodeRange{2625, 2626},
297    CodeRange{2631, 2632},
298    CodeRange{2635, 2637},
299    CodeRange{2641, 2641},
300    CodeRange{2672, 2673},
301    CodeRange{2677, 2677},
302    CodeRange{2689, 2690},
303    CodeRange{2748, 2748},
304    CodeRange{2753, 2757},
305    CodeRange{2759, 2760},
306    CodeRange{2765, 2765},
307    CodeRange{2786, 2787},
308    CodeRange{2810, 2815},
309    CodeRange{2817, 2817},
310    CodeRange{2876, 2876},
311    CodeRange{2879, 2879},
312    CodeRange{2881, 2884},
313    CodeRange{2893, 2893},
314    CodeRange{2901, 2902},
315    CodeRange{2914, 2915},
316    CodeRange{2946, 2946},
317    CodeRange{3008, 3008},
318    CodeRange{3021, 3021},
319    CodeRange{3072, 3072},
320    CodeRange{3076, 3076},
321    CodeRange{3132, 3132},
322    CodeRange{3134, 3136},
323    CodeRange{3142, 3144},
324    CodeRange{3146, 3149},
325    CodeRange{3157, 3158},
326    CodeRange{3170, 3171},
327    CodeRange{3201, 3201},
328    CodeRange{3260, 3260},
329    CodeRange{3263, 3263},
330    CodeRange{3270, 3270},
331    CodeRange{3276, 3277},
332    CodeRange{3298, 3299},
333    CodeRange{3328, 3329},
334    CodeRange{3387, 3388},
335    CodeRange{3393, 3396},
336    CodeRange{3405, 3405},
337    CodeRange{3426, 3427},
338    CodeRange{3457, 3457},
339    CodeRange{3530, 3530},
340    CodeRange{3538, 3540},
341    CodeRange{3542, 3542},
342    CodeRange{3633, 3633},
343    CodeRange{3636, 3642},
344    CodeRange{3655, 3662},
345    CodeRange{3761, 3761},
346    CodeRange{3764, 3772},
347    CodeRange{3784, 3790},
348    CodeRange{3864, 3865},
349    CodeRange{3893, 3893},
350    CodeRange{3895, 3895},
351    CodeRange{3897, 3897},
352    CodeRange{3953, 3966},
353    CodeRange{3968, 3972},
354    CodeRange{3974, 3975},
355    CodeRange{3981, 3991},
356    CodeRange{3993, 4028},
357    CodeRange{4038, 4038},
358    CodeRange{4141, 4144},
359    CodeRange{4146, 4151},
360    CodeRange{4153, 4154},
361    CodeRange{4157, 4158},
362    CodeRange{4184, 4185},
363    CodeRange{4190, 4192},
364    CodeRange{4209, 4212},
365    CodeRange{4226, 4226},
366    CodeRange{4229, 4230},
367    CodeRange{4237, 4237},
368    CodeRange{4253, 4253},
369    CodeRange{4957, 4959},
370    CodeRange{5906, 5908},
371    CodeRange{5938, 5939},
372    CodeRange{5970, 5971},
373    CodeRange{6002, 6003},
374    CodeRange{6068, 6069},
375    CodeRange{6071, 6077},
376    CodeRange{6086, 6086},
377    CodeRange{6089, 6099},
378    CodeRange{6109, 6109},
379    CodeRange{6155, 6157},
380    CodeRange{6159, 6159},
381    CodeRange{6277, 6278},
382    CodeRange{6313, 6313},
383    CodeRange{6432, 6434},
384    CodeRange{6439, 6440},
385    CodeRange{6450, 6450},
386    CodeRange{6457, 6459},
387    CodeRange{6679, 6680},
388    CodeRange{6683, 6683},
389    CodeRange{6742, 6742},
390    CodeRange{6744, 6750},
391    CodeRange{6752, 6752},
392    CodeRange{6754, 6754},
393    CodeRange{6757, 6764},
394    CodeRange{6771, 6780},
395    CodeRange{6783, 6783},
396    CodeRange{6832, 6862},
397    CodeRange{6912, 6915},
398    CodeRange{6964, 6964},
399    CodeRange{6966, 6970},
400    CodeRange{6972, 6972},
401    CodeRange{6978, 6978},
402    CodeRange{7019, 7027},
403    CodeRange{7040, 7041},
404    CodeRange{7074, 7077},
405    CodeRange{7080, 7081},
406    CodeRange{7083, 7085},
407    CodeRange{7142, 7142},
408    CodeRange{7144, 7145},
409    CodeRange{7149, 7149},
410    CodeRange{7151, 7153},
411    CodeRange{7212, 7219},
412    CodeRange{7222, 7223},
413    CodeRange{7376, 7378},
414    CodeRange{7380, 7392},
415    CodeRange{7394, 7400},
416    CodeRange{7405, 7405},
417    CodeRange{7412, 7412},
418    CodeRange{7416, 7417},
419    CodeRange{7616, 7679},
420    CodeRange{8400, 8432},
421    CodeRange{11503, 11505},
422    CodeRange{11647, 11647},
423    CodeRange{11744, 11775},
424    CodeRange{12330, 12333},
425    CodeRange{12441, 12442},
426    CodeRange{42607, 42610},
427    CodeRange{42612, 42621},
428    CodeRange{42654, 42655},
429    CodeRange{42736, 42737},
430    CodeRange{43010, 43010},
431    CodeRange{43014, 43014},
432    CodeRange{43019, 43019},
433    CodeRange{43045, 43046},
434    CodeRange{43052, 43052},
435    CodeRange{43204, 43205},
436    CodeRange{43232, 43249},
437    CodeRange{43263, 43263},
438    CodeRange{43302, 43309},
439    CodeRange{43335, 43345},
440    CodeRange{43392, 43394},
441    CodeRange{43443, 43443},
442    CodeRange{43446, 43449},
443    CodeRange{43452, 43453},
444    CodeRange{43493, 43493},
445    CodeRange{43561, 43566},
446    CodeRange{43569, 43570},
447    CodeRange{43573, 43574},
448    CodeRange{43587, 43587},
449    CodeRange{43596, 43596},
450    CodeRange{43644, 43644},
451    CodeRange{43696, 43696},
452    CodeRange{43698, 43700},
453    CodeRange{43703, 43704},
454    CodeRange{43710, 43711},
455    CodeRange{43713, 43713},
456    CodeRange{43756, 43757},
457    CodeRange{43766, 43766},
458    CodeRange{44005, 44005},
459    CodeRange{44008, 44008},
460    CodeRange{44013, 44013},
461    CodeRange{64286, 64286},
462    CodeRange{65024, 65039},
463    CodeRange{65056, 65071},
464    CodeRange{66045, 66045},
465    CodeRange{66272, 66272},
466    CodeRange{66422, 66426},
467    CodeRange{68097, 68099},
468    CodeRange{68101, 68102},
469    CodeRange{68108, 68111},
470    CodeRange{68152, 68154},
471    CodeRange{68159, 68159},
472    CodeRange{68325, 68326},
473    CodeRange{68900, 68903},
474    CodeRange{69291, 69292},
475    CodeRange{69373, 69375},
476    CodeRange{69446, 69456},
477    CodeRange{69506, 69509},
478    CodeRange{69633, 69633},
479    CodeRange{69688, 69702},
480    CodeRange{69744, 69744},
481    CodeRange{69747, 69748},
482    CodeRange{69759, 69761},
483    CodeRange{69811, 69814},
484    CodeRange{69817, 69818},
485    CodeRange{69826, 69826},
486    CodeRange{69888, 69890},
487    CodeRange{69927, 69931},
488    CodeRange{69933, 69940},
489    CodeRange{70003, 70003},
490    CodeRange{70016, 70017},
491    CodeRange{70070, 70078},
492    CodeRange{70089, 70092},
493    CodeRange{70095, 70095},
494    CodeRange{70191, 70193},
495    CodeRange{70196, 70196},
496    CodeRange{70198, 70199},
497    CodeRange{70206, 70206},
498    CodeRange{70209, 70209},
499    CodeRange{70367, 70367},
500    CodeRange{70371, 70378},
501    CodeRange{70400, 70401},
502    CodeRange{70459, 70460},
503    CodeRange{70464, 70464},
504    CodeRange{70502, 70508},
505    CodeRange{70512, 70516},
506    CodeRange{70712, 70719},
507    CodeRange{70722, 70724},
508    CodeRange{70726, 70726},
509    CodeRange{70750, 70750},
510    CodeRange{70835, 70840},
511    CodeRange{70842, 70842},
512    CodeRange{70847, 70848},
513    CodeRange{70850, 70851},
514    CodeRange{71090, 71093},
515    CodeRange{71100, 71101},
516    CodeRange{71103, 71104},
517    CodeRange{71132, 71133},
518    CodeRange{71219, 71226},
519    CodeRange{71229, 71229},
520    CodeRange{71231, 71232},
521    CodeRange{71339, 71339},
522    CodeRange{71341, 71341},
523    CodeRange{71344, 71349},
524    CodeRange{71351, 71351},
525    CodeRange{71453, 71455},
526    CodeRange{71458, 71461},
527    CodeRange{71463, 71467},
528    CodeRange{71727, 71735},
529    CodeRange{71737, 71738},
530    CodeRange{71995, 71996},
531    CodeRange{71998, 71998},
532    CodeRange{72003, 72003},
533    CodeRange{72148, 72151},
534    CodeRange{72154, 72155},
535    CodeRange{72160, 72160},
536    CodeRange{72193, 72202},
537    CodeRange{72243, 72248},
538    CodeRange{72251, 72254},
539    CodeRange{72263, 72263},
540    CodeRange{72273, 72278},
541    CodeRange{72281, 72283},
542    CodeRange{72330, 72342},
543    CodeRange{72344, 72345},
544    CodeRange{72752, 72758},
545    CodeRange{72760, 72765},
546    CodeRange{72767, 72767},
547    CodeRange{72850, 72871},
548    CodeRange{72874, 72880},
549    CodeRange{72882, 72883},
550    CodeRange{72885, 72886},
551    CodeRange{73009, 73014},
552    CodeRange{73018, 73018},
553    CodeRange{73020, 73021},
554    CodeRange{73023, 73029},
555    CodeRange{73031, 73031},
556    CodeRange{73104, 73105},
557    CodeRange{73109, 73109},
558    CodeRange{73111, 73111},
559    CodeRange{73459, 73460},
560    CodeRange{73472, 73473},
561    CodeRange{73526, 73530},
562    CodeRange{73536, 73536},
563    CodeRange{73538, 73538},
564    CodeRange{78912, 78912},
565    CodeRange{78919, 78933},
566    CodeRange{92912, 92916},
567    CodeRange{92976, 92982},
568    CodeRange{94031, 94031},
569    CodeRange{94095, 94098},
570    CodeRange{94180, 94180},
571    CodeRange{113821, 113822},
572    CodeRange{118528, 118573},
573    CodeRange{118576, 118598},
574    CodeRange{119143, 119145},
575    CodeRange{119163, 119170},
576    CodeRange{119173, 119179},
577    CodeRange{119210, 119213},
578    CodeRange{119362, 119364},
579    CodeRange{121344, 121398},
580    CodeRange{121403, 121452},
581    CodeRange{121461, 121461},
582    CodeRange{121476, 121476},
583    CodeRange{121499, 121503},
584    CodeRange{121505, 121519},
585    CodeRange{122880, 122886},
586    CodeRange{122888, 122904},
587    CodeRange{122907, 122913},
588    CodeRange{122915, 122916},
589    CodeRange{122918, 122922},
590    CodeRange{123023, 123023},
591    CodeRange{123184, 123190},
592    CodeRange{123566, 123566},
593    CodeRange{123628, 123631},
594    CodeRange{124140, 124143},
595    CodeRange{125136, 125142},
596    CodeRange{125252, 125258},
597    CodeRange{917760, 917999},
598  ], code)
599
600# Unicode 15.1 Extended_Pictographic ranges, including reserved ranges.
601# Source: https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-data.txt
602def unicode_is_pictographic(code: U32) -> Bool:
603  contains_range([
604    CodeRange{169, 169},
605    CodeRange{174, 174},
606    CodeRange{8252, 8252},
607    CodeRange{8265, 8265},
608    CodeRange{8482, 8482},
609    CodeRange{8505, 8505},
610    CodeRange{8596, 8601},
611    CodeRange{8617, 8618},
612    CodeRange{8986, 8987},
613    CodeRange{9000, 9000},
614    CodeRange{9096, 9096},
615    CodeRange{9167, 9167},
616    CodeRange{9193, 9203},
617    CodeRange{9208, 9210},
618    CodeRange{9410, 9410},
619    CodeRange{9642, 9643},
620    CodeRange{9654, 9654},
621    CodeRange{9664, 9664},
622    CodeRange{9723, 9726},
623    CodeRange{9728, 9733},
624    CodeRange{9735, 9746},
625    CodeRange{9748, 9861},
626    CodeRange{9872, 9989},
627    CodeRange{9992, 10002},
628    CodeRange{10004, 10004},
629    CodeRange{10006, 10006},
630    CodeRange{10013, 10013},
631    CodeRange{10017, 10017},
632    CodeRange{10024, 10024},
633    CodeRange{10035, 10036},
634    CodeRange{10052, 10052},
635    CodeRange{10055, 10055},
636    CodeRange{10060, 10060},
637    CodeRange{10062, 10062},
638    CodeRange{10067, 10069},
639    CodeRange{10071, 10071},
640    CodeRange{10083, 10087},
641    CodeRange{10133, 10135},
642    CodeRange{10145, 10145},
643    CodeRange{10160, 10160},
644    CodeRange{10175, 10175},
645    CodeRange{10548, 10549},
646    CodeRange{11013, 11015},
647    CodeRange{11035, 11036},
648    CodeRange{11088, 11088},
649    CodeRange{11093, 11093},
650    CodeRange{12336, 12336},
651    CodeRange{12349, 12349},
652    CodeRange{12951, 12951},
653    CodeRange{12953, 12953},
654    CodeRange{126976, 127231},
655    CodeRange{127245, 127247},
656    CodeRange{127279, 127279},
657    CodeRange{127340, 127345},
658    CodeRange{127358, 127359},
659    CodeRange{127374, 127374},
660    CodeRange{127377, 127386},
661    CodeRange{127405, 127461},
662    CodeRange{127489, 127503},
663    CodeRange{127514, 127514},
664    CodeRange{127535, 127535},
665    CodeRange{127538, 127546},
666    CodeRange{127548, 127551},
667    CodeRange{127561, 127994},
668    CodeRange{128000, 128317},
669    CodeRange{128326, 128591},
670    CodeRange{128640, 128767},
671    CodeRange{128884, 128895},
672    CodeRange{128981, 129023},
673    CodeRange{129036, 129039},
674    CodeRange{129096, 129103},
675    CodeRange{129114, 129119},
676    CodeRange{129160, 129167},
677    CodeRange{129198, 129279},
678    CodeRange{129292, 129338},
679    CodeRange{129340, 129349},
680    CodeRange{129351, 129791},
681    CodeRange{130048, 131069},
682  ], code)
683
684# Unicode 15.1 Emoji_Modifier_Base ranges: the emoji a skin tone modifies.
685# Source: https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-data.txt
686def unicode_takes_tone(code: U32) -> Bool:
687  contains_range([
688    CodeRange{9757, 9757},
689    CodeRange{9977, 9977},
690    CodeRange{9994, 9997},
691    CodeRange{127877, 127877},
692    CodeRange{127938, 127940},
693    CodeRange{127943, 127943},
694    CodeRange{127946, 127948},
695    CodeRange{128066, 128067},
696    CodeRange{128070, 128080},
697    CodeRange{128102, 128120},
698    CodeRange{128124, 128124},
699    CodeRange{128129, 128131},
700    CodeRange{128133, 128135},
701    CodeRange{128143, 128143},
702    CodeRange{128145, 128145},
703    CodeRange{128170, 128170},
704    CodeRange{128372, 128373},
705    CodeRange{128378, 128378},
706    CodeRange{128400, 128400},
707    CodeRange{128405, 128406},
708    CodeRange{128581, 128583},
709    CodeRange{128587, 128591},
710    CodeRange{128675, 128675},
711    CodeRange{128692, 128694},
712    CodeRange{128704, 128704},
713    CodeRange{128716, 128716},
714    CodeRange{129292, 129292},
715    CodeRange{129295, 129295},
716    CodeRange{129304, 129311},
717    CodeRange{129318, 129318},
718    CodeRange{129328, 129337},
719    CodeRange{129340, 129342},
720    CodeRange{129399, 129399},
721    CodeRange{129461, 129462},
722    CodeRange{129464, 129465},
723    CodeRange{129467, 129467},
724    CodeRange{129485, 129487},
725    CodeRange{129489, 129501},
726    CodeRange{129731, 129733},
727    CodeRange{129776, 129784},
728  ], code)
729
730# Unicode 15.1 code points with an emoji presentation sequence (VS16) that
731# are not East Asian Wide or Fullwidth, so VS16 widens them to 2.
732# Sources: https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-variation-sequences.txt
733# and https://www.unicode.org/Public/15.1.0/ucd/EastAsianWidth.txt
734def unicode_widens_with_vs16(code: U32) -> Bool:
735  contains_range([
736    CodeRange{35, 35},
737    CodeRange{42, 42},
738    CodeRange{48, 57},
739    CodeRange{169, 169},
740    CodeRange{174, 174},
741    CodeRange{8252, 8252},
742    CodeRange{8265, 8265},
743    CodeRange{8482, 8482},
744    CodeRange{8505, 8505},
745    CodeRange{8596, 8601},
746    CodeRange{8617, 8618},
747    CodeRange{9000, 9000},
748    CodeRange{9167, 9167},
749    CodeRange{9197, 9199},
750    CodeRange{9201, 9202},
751    CodeRange{9208, 9210},
752    CodeRange{9410, 9410},
753    CodeRange{9642, 9643},
754    CodeRange{9654, 9654},
755    CodeRange{9664, 9664},
756    CodeRange{9723, 9724},
757    CodeRange{9728, 9732},
758    CodeRange{9742, 9742},
759    CodeRange{9745, 9745},
760    CodeRange{9752, 9752},
761    CodeRange{9757, 9757},
762    CodeRange{9760, 9760},
763    CodeRange{9762, 9763},
764    CodeRange{9766, 9766},
765    CodeRange{9770, 9770},
766    CodeRange{9774, 9775},
767    CodeRange{9784, 9786},
768    CodeRange{9792, 9792},
769    CodeRange{9794, 9794},
770    CodeRange{9823, 9824},
771    CodeRange{9827, 9827},
772    CodeRange{9829, 9830},
773    CodeRange{9832, 9832},
774    CodeRange{9851, 9851},
775    CodeRange{9854, 9854},
776    CodeRange{9874, 9874},
777    CodeRange{9876, 9879},
778    CodeRange{9881, 9881},
779    CodeRange{9883, 9884},
780    CodeRange{9888, 9888},
781    CodeRange{9895, 9895},
782    CodeRange{9904, 9905},
783    CodeRange{9928, 9928},
784    CodeRange{9935, 9935},
785    CodeRange{9937, 9937},
786    CodeRange{9939, 9939},
787    CodeRange{9961, 9961},
788    CodeRange{9968, 9969},
789    CodeRange{9972, 9972},
790    CodeRange{9975, 9977},
791    CodeRange{9986, 9986},
792    CodeRange{9992, 9993},
793    CodeRange{9996, 9997},
794    CodeRange{9999, 9999},
795    CodeRange{10002, 10002},
796    CodeRange{10004, 10004},
797    CodeRange{10006, 10006},
798    CodeRange{10013, 10013},
799    CodeRange{10017, 10017},
800    CodeRange{10035, 10036},
801    CodeRange{10052, 10052},
802    CodeRange{10055, 10055},
803    CodeRange{10083, 10084},
804    CodeRange{10145, 10145},
805    CodeRange{10548, 10549},
806    CodeRange{11013, 11015},
807    CodeRange{127344, 127345},
808    CodeRange{127358, 127359},
809    CodeRange{127777, 127777},
810    CodeRange{127780, 127788},
811    CodeRange{127798, 127798},
812    CodeRange{127869, 127869},
813    CodeRange{127894, 127895},
814    CodeRange{127897, 127899},
815    CodeRange{127902, 127903},
816    CodeRange{127947, 127950},
817    CodeRange{127956, 127967},
818    CodeRange{127987, 127987},
819    CodeRange{127989, 127989},
820    CodeRange{127991, 127991},
821    CodeRange{128063, 128063},
822    CodeRange{128065, 128065},
823    CodeRange{128253, 128253},
824    CodeRange{128329, 128330},
825    CodeRange{128367, 128368},
826    CodeRange{128371, 128377},
827    CodeRange{128391, 128391},
828    CodeRange{128394, 128397},
829    CodeRange{128400, 128400},
830    CodeRange{128421, 128421},
831    CodeRange{128424, 128424},
832    CodeRange{128433, 128434},
833    CodeRange{128444, 128444},
834    CodeRange{128450, 128452},
835    CodeRange{128465, 128467},
836    CodeRange{128476, 128478},
837    CodeRange{128481, 128481},
838    CodeRange{128483, 128483},
839    CodeRange{128488, 128488},
840    CodeRange{128495, 128495},
841    CodeRange{128499, 128499},
842    CodeRange{128506, 128506},
843    CodeRange{128715, 128715},
844    CodeRange{128717, 128719},
845    CodeRange{128736, 128741},
846    CodeRange{128745, 128745},
847    CodeRange{128752, 128752},
848    CodeRange{128755, 128755},
849  ], code)
850
851# A code point's own width: controls, marks, 200B-200F and tag characters
852# (E0000-E007F) are 0, East Asian Wide and Fullwidth are 2, the rest 1.
853def code_width_by(small: Bool, +code: U32) -> Nat:
854  match small:
855    case True{}:
856      Bool.pick(Nat, Bool.or(U32.is_le(code, 31),
857        Bool.and(U32.is_ge(code, 127), U32.is_le(code, 159))), 0n, 1n)
858    case False{}:
859      Bool.pick(Nat, Bool.or(unicode_is_zero_mark(code),
860        Bool.or(Bool.and(U32.is_ge(code, 8203), U32.is_le(code, 8207)),
861          Bool.and(U32.is_ge(code, 917504), U32.is_le(code, 917631)))), 0n,
862        Bool.pick(Nat, unicode_is_wide(code), 2n, 1n))
863
864def code_width(+code: U32) -> Nat:
865  code_width_by(U32.is_lt(code, 768), code)
866
867# What a code point can do in an emoji sequence (UTS #51): start a keycap
868# (#, * and 0-9), be a pictograph, a skin tone, a ZWJ or VS16, or none.
869type CodeClass is Data:
870  PlainCode{}
871  KeycapCode{}
872  PictoCode{}
873  ToneCode{}
874  JoinerCode{}
875  Vs16Code{}
876
877# Where width stands in an emoji sequence: outside one; after an emoji, with
878# the code a skin tone or VS16 applies to, whether it counts 1 so far, and
879# whether a ZWJ may join it; after such a ZWJ; or after a pictograph that a
880# ZWJ joined, which adds nothing.
881type EmojiRun is Data:
882  EmojiNone{}
883  EmojiBase{code: U32, narrow: Bool, join: Bool}
884  EmojiJoiner{}
885  EmojiJoined{code: U32}
886
887type WidthStep is Data:
888  WidthStep{width: Nat, run: EmojiRun}
889
890def keycap_base(+code: U32) -> Bool:
891  Bool.or(Bool.or(U32.is_eq(code, 35), U32.is_eq(code, 42)),
892    Bool.and(U32.is_ge(code, 48), U32.is_le(code, 57)))
893
894# may: whether code is in 203C-3299 or from 1F000, which hold every
895# pictograph from 768 on.
896def picto_class(may: Bool, +code: U32) -> CodeClass:
897  match may:
898    case True{}:
899      Bool.pick(CodeClass, unicode_is_pictographic(code), PictoCode{},
900        PlainCode{})
901    case False{}: PlainCode{}
902
903def code_class_by(small: Bool, +code: U32) -> CodeClass:
904  match small:
905    case True{}:
906      Bool.pick(CodeClass, keycap_base(code), KeycapCode{},
907        Bool.pick(CodeClass, Bool.or(U32.is_eq(code, 169),
908          U32.is_eq(code, 174)), PictoCode{}, PlainCode{}))
909    case False{}:
910      Bool.pick(CodeClass, U32.is_eq(code, 65039), Vs16Code{},
911        Bool.pick(CodeClass, U32.is_eq(code, 8205), JoinerCode{},
912          Bool.pick(CodeClass, Bool.and(U32.is_ge(code, 127995),
913            U32.is_le(code, 127999)), ToneCode{},
914            picto_class(Bool.or(Bool.and(U32.is_ge(code, 8252),
915              U32.is_le(code, 12953)), U32.is_ge(code, 126976)), code))))
916
917def code_class(+code: U32) -> CodeClass:
918  code_class_by(U32.is_lt(code, 768), code)
919
920def picto_alone(+wide: Bool, code: U32) -> WidthStep:
921  WidthStep{Bool.pick(Nat, wide, 2n, 1n),
922    EmojiBase{code, Bool.not(wide), True{}}}
923
924# A pictograph after a ZWJ joins the sequence; any other starts an emoji.
925def picto_step(run: EmojiRun, +code: U32) -> WidthStep:
926  match run:
927    case EmojiNone{}: picto_alone(unicode_is_wide(code), code)
928    case EmojiBase{base, narrow, join}:
929      picto_alone(unicode_is_wide(code), code)
930    case EmojiJoiner{}: WidthStep{0n, EmojiJoined{code}}
931    case EmojiJoined{base}: picto_alone(unicode_is_wide(code), code)
932
933# A skin tone after an emoji that takes one widens it to 2; any other is
934# 2 wide alone, as East Asian Wide.
935def tone_on(takes: Bool, narrow: Bool, join: Bool, code: U32) ->
936    WidthStep:
937  match takes:
938    case True{}:
939      WidthStep{Bool.pick(Nat, narrow, 1n, 0n), EmojiBase{code, False{}, join}}
940    case False{}: WidthStep{2n, EmojiNone{}}
941
942def tone_on_joined(takes: Bool, code: U32) -> WidthStep:
943  match takes:
944    case True{}: WidthStep{0n, EmojiJoined{code}}
945    case False{}: WidthStep{2n, EmojiNone{}}
946
947def tone_step(run: EmojiRun, code: U32) -> WidthStep:
948  match run:
949    case EmojiNone{}: WidthStep{2n, EmojiNone{}}
950    case EmojiBase{base, narrow, join}:
951      tone_on(unicode_takes_tone(base), narrow, join, code)
952    case EmojiJoiner{}: WidthStep{2n, EmojiNone{}}
953    case EmojiJoined{base}: tone_on_joined(unicode_takes_tone(base), code)
954
955# A ZWJ after an emoji that may join, or after a joined pictograph, waits
956# for the next pictograph.
957def joiner_run(run: EmojiRun) -> EmojiRun:
958  match run:
959    case EmojiNone{}: EmojiNone{}
960    case EmojiBase{code, narrow, join}:
961      Bool.pick(EmojiRun, join, EmojiJoiner{}, EmojiNone{})
962    case EmojiJoiner{}: EmojiNone{}
963    case EmojiJoined{code}: EmojiJoiner{}
964
965def vs16_widen(widens: Bool, code: U32, join: Bool) -> WidthStep:
966  match widens:
967    case True{}: WidthStep{1n, EmojiBase{code, False{}, join}}
968    case False{}: WidthStep{0n, EmojiBase{code, True{}, join}}
969
970def vs16_on(narrow: Bool, +code: U32, join: Bool) -> WidthStep:
971  match narrow:
972    case True{}: vs16_widen(unicode_widens_with_vs16(code), code, join)
973    case False{}: WidthStep{0n, EmojiBase{code, False{}, join}}
974
975# VS16 widens a 1-wide emoji with an emoji presentation to 2, once.
976def vs16_step(run: EmojiRun) -> WidthStep:
977  match run:
978    case EmojiNone{}: WidthStep{0n, EmojiNone{}}
979    case EmojiBase{code, narrow, join}: vs16_on(narrow, code, join)
980    case EmojiJoiner{}: WidthStep{0n, EmojiNone{}}
981    case EmojiJoined{code}: WidthStep{0n, EmojiJoined{code}}
982
983def code_step(cls: CodeClass, run: EmojiRun, code: U32) -> WidthStep:
984  match cls:
985    case PlainCode{}: WidthStep{code_width(code), EmojiNone{}}
986    case KeycapCode{}: WidthStep{1n, EmojiBase{code, True{}, False{}}}
987    case PictoCode{}: picto_step(run, code)
988    case ToneCode{}: tone_step(run, code)
989    case JoinerCode{}: WidthStep{0n, joiner_run(run)}
990    case Vs16Code{}: vs16_step(run)
991
992# acc, plus the width of step, the last code point's, and of s after it.
993def width_go(s: String, step: WidthStep, acc: Nat) -> Nat:
994  match s step:
995    case SNil{} WidthStep{w, run}: Nat.add(w, acc)
996    case SCon{head, tail} WidthStep{w, run}:
997      +code = Char.to_u32(head)
998      width_go(tail, code_step(code_class(code), run, code), Nat.add(w, acc))
999
1000# Display columns, as Rust's unicode-width 0.2 counts them: each code point's
1001# own width, except that an emoji sequence is one cell 2 wide. A pictograph
1002# that a ZWJ joins, a skin tone after an emoji that takes one, and tag
1003# characters add nothing; VS16 widens a 1-wide emoji to 2.
1004def width(s: String) -> Nat:
1005  width_go(s, WidthStep{0n, EmojiNone{}}, 0n)
1006
1007def leading_indent(s: String) -> String:
1008  match s:
1009    case SNil{}:
1010      ""
1011    case SCon{+head, tail}:
1012      Bool.pick(String, Char.is_eq(head, ' '),
1013        SCon{head, leading_indent(tail)}, "")
1014
1015type WrapState is Data:
1016  WrapState{indent: String, cols: Nat, line: String,
1017    started: Bool, done: List<&2, String>}
1018
1019def wrap_words(words: List<&2, String>, state: WrapState) -> String:
1020  match words state:
1021    case Nil{} WrapState{indent, cols, line, started, done}:
1022      Bool.pick(String, started,
1023        String.join(List.reverse(&2, String, line <> done), "\n"), "")
1024    case +word <> rest WrapState{+indent, +cols, +line, +started, +done}:
1025      +candidate = Bool.pick(String, started,
1026        String.append(line, String.append(" ", word)),
1027        String.append(indent, word))
1028      fits = Bool.or(Bool.not(started), Nat.is_le(width(strip(candidate)), cols))
1029      next = Bool.pick(WrapState, fits,
1030        WrapState{indent, cols, candidate, True{}, done},
1031        WrapState{indent, cols, String.append(indent, word), True{}, line <> done})
1032      wrap_words(rest, next)
1033
1034def words(s: String) -> List<&2, String>:
1035  List.filter(~String, ~(w => Bool.not(String.is_empty(w))),
1036    String.split(s, ' '))
1037
1038def wrap_line(+line: String, cols: Nat) -> String:
1039  +indent = leading_indent(line)
1040  body = String.drop(line, String.length(indent))
1041  parts = words(body)
1042  wrap_words(parts, WrapState{indent, cols, indent, False{}, Nil{}})
1043
1044def wrap_lines(lines: List<&2, String>, +cols: Nat) -> List<&2, String>:
1045  match lines:
1046    case Nil{}:
1047      Nil{}
1048    case head <> tail:
1049      wrap_line(head, cols) <> wrap_lines(tail, cols)
1050
1051def wrap(text: String, cols: Nat) -> String:
1052  String.join(wrap_lines(String.lines(text), cols), "\n")
1053
1054type ValueType is Data:
1055  TNat{}
1056  TU32{}
1057  TF32{}
1058  TBool{}
1059  TFile{}
1060  TDir{}
1061
1062def type_name(t: ValueType) -> String:
1063  match t:
1064    case TNat{}: "as_nat"
1065    case TU32{}: "as_u32"
1066    case TF32{}: "as_f32"
1067    case TBool{}: "as_bool"
1068    case TFile{}: "as_file"
1069    case TDir{}: "as_dir"
1070
1071type Item is Data:
1072  Arg{usage: String, help: String, items: List<&2, Item>}
1073  Sub{name: String, about: String, items: List<&2, Item>}
1074  Version{value: String}
1075  Alias{name: String}
1076  Hidden{}
1077  Example{line: String, what: String}
1078  After{text: String}
1079  SubOptional{}
1080  External{}
1081  Exclusive{group: String}
1082  NeedOne{group: String}
1083  Id{value: String}
1084  Required{}
1085  AttrDefault{value: String}
1086  AttrEnv{name: String}
1087  Global{}
1088  Heading{title: String}
1089  As{t: ValueType}
1090  OneOf{values: List<&2, String>}
1091  Delim{sep: Char}
1092  Hyphen{}
1093  Rest{}
1094  Bare{value: String}
1095  Conflicts{id: String}
1096  Requires{id: String}
1097  Group{name: String}
1098  Builtin{}
1099
1100type Cmd is Data:
1101  Cmd{name: String, about: String, items: List<&2, Item>}
1102
1103type Problem is Data:
1104  BadUsage{at: String, usage: String, col: Nat}
1105  BadName{at: String, name: String}
1106  Dup{at: String, name: String}
1107  Reserved{at: String, name: String}
1108  Misplaced{at: String, attr: String}
1109  NoHelp{at: String, what: String}
1110  BadDefault{at: String, id: String, value: String}
1111  BadRef{at: String, id: String}
1112  BadPositional{at: String, id: String, why: String}
1113  Mixed{at: String}
1114  BadChoices{at: String, id: String}
1115  BadChoice{at: String, id: String, value: String}
1116  NeedBare{at: String, id: String}
1117  DecodeBug{at: String, msg: String}
1118
1119def cmd(name: String, about: String, items: List<&2, Item>) -> Cmd:
1120  Cmd{name, about, items}
1121
1122def sub(name: String, about: String, items: List<&2, Item>) -> Item:
1123  Sub{name, about, items}
1124
1125def arg(usage: String, help: String, items: List<&2, Item>) -> Item:
1126  Arg{usage, help, items}
1127
1128def version(value: String) -> Item:
1129  Version{value}
1130
1131def alias(name: String) -> Item:
1132  Alias{name}
1133
1134def hidden() -> Item:
1135  Hidden{}
1136
1137def example(line: String, what: String) -> Item:
1138  Example{line, what}
1139
1140def after(text: String) -> Item:
1141  After{text}
1142
1143def sub_optional() -> Item:
1144  SubOptional{}
1145
1146def external() -> Item:
1147  External{}
1148
1149def exclusive(group: String) -> Item:
1150  Exclusive{group}
1151
1152def need_one(group: String) -> Item:
1153  NeedOne{group}
1154
1155def id(value: String) -> Item:
1156  Id{value}
1157
1158def required() -> Item:
1159  Required{}
1160
1161def default(value: String) -> Item:
1162  AttrDefault{value}
1163
1164def env(name: String) -> Item:
1165  AttrEnv{name}
1166
1167def global() -> Item:
1168  Global{}
1169
1170def heading(title: String) -> Item:
1171  Heading{title}
1172
1173def as_nat() -> Item:
1174  As{TNat{}}
1175
1176def as_u32() -> Item:
1177  As{TU32{}}
1178
1179def as_f32() -> Item:
1180  As{TF32{}}
1181
1182def as_bool() -> Item:
1183  As{TBool{}}
1184
1185def as_file() -> Item:
1186  As{TFile{}}
1187
1188def as_dir() -> Item:
1189  As{TDir{}}
1190
1191def one_of(values: List<&2, String>) -> Item:
1192  OneOf{values}
1193
1194def delim(sep: Char) -> Item:
1195  Delim{sep}
1196
1197def hyphen() -> Item:
1198  Hyphen{}
1199
1200def rest() -> Item:
1201  Rest{}
1202
1203def bare(value: String) -> Item:
1204  Bare{value}
1205
1206def conflicts(target: String) -> Item:
1207  Conflicts{target}
1208
1209def requires(target: String) -> Item:
1210  Requires{target}
1211
1212def group(name: String) -> Item:
1213  Group{name}
1214
1215def color() -> Item:
1216  arg("--color <WHEN>", "When to use color", [one_of(["auto", "always", "never"]), default("auto"), global()])
1217
1218# Builtin{} marks the subcommands that only eval acts on (§6.4).
1219def completions() -> Item:
1220  sub("completions", "Print a shell completion script", [Builtin{}, arg("<SHELL>", "Shell to generate for", [one_of(["bash", "zsh", "fish"])])])
1221
1222def man() -> Item:
1223  sub("man", "Print the man page", [Builtin{}, hidden(), arg("[COMMAND]...", "Subcommand path", [])])
1224
1225type UsageKind is Data:
1226  FlagKind{}
1227  CountKind{}
1228  NegatableKind{}
1229  OptionKind{many: Bool}
1230  BareOptionKind{}
1231  PosKind{required: Bool, many: Bool}
1232
1233type Usage is Data:
1234  Parsed{id: String, short: String, long: String, kind: UsageKind}
1235  Invalid{col: Nat}
1236
1237type NameScan is Data:
1238  NameOK{name: String, rest: String, next: Nat}
1239  NameBad{col: Nat}
1240
1241# PhOK's name is reversed: only a positional's name is read.
1242type PhScan is Data:
1243  PhOK{rev: String, rest: String, next: Nat}
1244  PhBad{col: Nat}
1245
1246def name_start(+c: Char) -> Bool:
1247  Bool.or(Char.is_alpha(c), Char.is_digit(c))
1248
1249def name_char(+c: Char) -> Bool:
1250  Bool.or(name_start(c), Bool.or(Char.is_eq(c, '-'),
1251    Bool.or(Char.is_eq(c, '_'), Char.is_eq(c, '.'))))
1252
1253# rev is the name so far, reversed.
1254def finish_name(rev: String, rest: String, +pos: Nat,
1255    +first: Bool, +dot_start: Nat) -> NameScan:
1256  Bool.pick(NameScan, Bool.or(first, Nat.is_lt(dot_start, pos)),
1257    NameBad{Bool.pick(Nat, first, pos, dot_start)},
1258    NameOK{String.reverse(rev), rest, pos})
1259
1260def scan_name(s: String, +pos: Nat, rev: String,
1261    +first: Bool, dot_start: Nat) -> NameScan:
1262  match s:
1263    case SNil{}:
1264      finish_name(rev, SNil{}, pos, first, dot_start)
1265    case SCon{Chr{32}, tail}:
1266      finish_name(rev, SCon{Char.from_u32(32), tail}, pos, first, dot_start)
1267    case SCon{Chr{91}, tail}:
1268      finish_name(rev, SCon{Char.from_u32(91), tail}, pos, first, dot_start)
1269    case SCon{+h, tail}:
1270      valid = Bool.pick(Bool, first, name_start(h), name_char(h))
1271      next_dot = Bool.pick(Nat, Char.is_eq(h, '.'), dot_start, Nat.add(pos, 1n))
1272      Bool.pick(NameScan, valid,
1273        scan_name(tail, Nat.add(pos, 1n), SCon{h, rev}, False{}, next_dot),
1274        NameBad{pos})
1275
1276# A placeholder ends at a character no name holds: at close after a name,
1277# else with an error there.
1278def ph_stop(at_close: Bool, rev: String, rest: String, +pos: Nat) -> PhScan:
1279  match at_close rev:
1280    case True{} SCon{h, t}: PhOK{SCon{h, t}, rest, Nat.add(pos, 1n)}
1281    case _ _: PhBad{pos}
1282
1283# The placeholder name in s up to close, reversed in rev. A name holds no
1284# space, '<', '>', '[', ']' or '=', and close is one of them.
1285def scan_ph(s: String, +pos: Nat, rev: String, +close: Char) -> PhScan:
1286  match s:
1287    case SNil{}: PhBad{pos}
1288    case SCon{Chr{32}, tail}: ph_stop(Char.is_eq(' ', close), rev, tail, pos)
1289    case SCon{Chr{60}, tail}: ph_stop(Char.is_eq('<', close), rev, tail, pos)
1290    case SCon{Chr{62}, tail}: ph_stop(Char.is_eq('>', close), rev, tail, pos)
1291    case SCon{Chr{91}, tail}: ph_stop(Char.is_eq('[', close), rev, tail, pos)
1292    case SCon{Chr{93}, tail}: ph_stop(Char.is_eq(']', close), rev, tail, pos)
1293    case SCon{Chr{61}, tail}: ph_stop(Char.is_eq('=', close), rev, tail, pos)
1294    case SCon{h, tail}: scan_ph(tail, Nat.add(pos, 1n), SCon{h, rev}, close)
1295
1296def finish_pos(found: PhScan, kind: UsageKind) -> Usage:
1297  match found:
1298    case PhBad{col}:
1299      Invalid{col}
1300    case PhOK{rev, SNil{}, next}:
1301      Parsed{String.to_lower(String.reverse(rev)), "", "", kind}
1302    case PhOK{rev, rest, next}:
1303      Invalid{next}
1304
1305def parse_pos(s: String, +repeat: Bool) -> Usage:
1306  match s:
1307    case SCon{Chr{60}, tail}:
1308      finish_pos(scan_ph(tail, 1n, "", '>'),
1309        PosKind{True{}, repeat})
1310    case SCon{Chr{91}, tail}:
1311      finish_pos(scan_ph(tail, 1n, "", ']'),
1312        PosKind{False{}, repeat})
1313    case _:
1314      Invalid{0n}
1315
1316def finish_required_value(found: PhScan, id: String, short: String,
1317    long: String, kind: UsageKind) -> Usage:
1318  match found:
1319    case PhBad{col}:
1320      Invalid{col}
1321    case PhOK{rev, SNil{}, next}:
1322      Parsed{id, short, long, kind}
1323    case PhOK{rev, rest, next}:
1324      Invalid{next}
1325
1326def finish_optional_value(found: PhScan, id: String, short: String,
1327    long: String) -> Usage:
1328  match found:
1329    case PhBad{col}:
1330      Invalid{col}
1331    case PhOK{rev, SCon{Chr{93}, SNil{}}, next}:
1332      Parsed{id, short, long, BareOptionKind{}}
1333    case PhOK{rev, rest, next}:
1334      Invalid{next}
1335
1336def finish_long(found: NameScan, short: String,
1337    +repeat: Bool, +negatable: Bool) -> Usage:
1338  match found:
1339    case NameBad{col}:
1340      Invalid{col}
1341    case NameOK{+name, SNil{}, next}:
1342      Parsed{name, short, String.append("--", name),
1343        Bool.pick(UsageKind, negatable, NegatableKind{},
1344          Bool.pick(UsageKind, repeat, CountKind{}, FlagKind{}))}
1345    case NameOK{+name, SCon{Chr{32}, SCon{Chr{60}, tail}}, +next}:
1346      Bool.pick(Usage, negatable, Invalid{next},
1347        finish_required_value(scan_ph(tail, Nat.add(next, 2n), "", '>'),
1348          name, short, String.append("--", name),
1349          OptionKind{repeat}))
1350    case NameOK{+name, SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, tail}}}, +next}:
1351      Bool.pick(Usage, Bool.or(negatable, repeat), Invalid{next},
1352        finish_optional_value(scan_ph(tail, Nat.add(next, 3n), "", '>'),
1353          name, short, String.append("--", name)))
1354    case NameOK{name, rest, next}:
1355      Invalid{next}
1356
1357# A long name after "--[no-]" or "--", at column start.
1358def parse_long_prefixed(s: String, short: String,
1359    repeat: Bool, +start: Nat) -> Usage:
1360  match s:
1361    case SCon{Chr{45}, SCon{Chr{45}, SCon{Chr{91}, SCon{Chr{110},
1362        SCon{Chr{111}, SCon{Chr{45}, SCon{Chr{93}, rest}}}}}}}:
1363      finish_long(scan_name(rest, Nat.add(start, 7n), "", True{},
1364        Nat.add(start, 7n)), short, repeat, True{})
1365    case SCon{Chr{45}, SCon{Chr{45}, rest}}:
1366      finish_long(scan_name(rest, Nat.add(start, 2n), "", True{},
1367        Nat.add(start, 2n)), short, repeat, False{})
1368    case _:
1369      Invalid{start}
1370
1371def parse_short_tail(tail: String, +short: String, +repeat: Bool) -> Usage:
1372  match tail:
1373    case SNil{}:
1374      Parsed{String.drop(short, 1n), short, "",
1375        Bool.pick(UsageKind, repeat, CountKind{}, FlagKind{})}
1376    case SCon{Chr{44}, SCon{Chr{32}, long_rest}}:
1377      parse_long_prefixed(long_rest, short, repeat, 4n)
1378    case SCon{Chr{32}, SCon{Chr{60}, ph_rest}}:
1379      finish_required_value(scan_ph(ph_rest, 4n, "", '>'),
1380        String.drop(short, 1n), short, "",
1381        OptionKind{repeat})
1382    case SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, ph_rest}}}:
1383      finish_optional_value(scan_ph(ph_rest, 5n, "", '>'),
1384        String.drop(short, 1n), short, "")
1385    case _:
1386      Invalid{2n}
1387
1388def parse_short(s: String, repeat: Bool) -> Usage:
1389  match s:
1390    case SCon{Chr{45}, SCon{+h, tail}}:
1391      Bool.pick(Usage, name_start(h),
1392        parse_short_tail(tail, String.append("-", Char.show(h)), repeat),
1393        Invalid{1n})
1394    case _:
1395      Invalid{1n}
1396
1397def parse_usage_body(s: String, +repeat: Bool) -> Usage:
1398  match s:
1399    case SCon{Chr{60}, tail}:
1400      parse_pos(SCon{Char.from_u32(60), tail}, repeat)
1401    case SCon{Chr{91}, tail}:
1402      parse_pos(SCon{Char.from_u32(91), tail}, repeat)
1403    case SCon{Chr{45}, SCon{Chr{45}, tail}}:
1404      parse_long_prefixed(String.append("--", tail), "", repeat, 0n)
1405    case SCon{Chr{45}, tail}:
1406      parse_short(SCon{Char.from_u32(45), tail}, repeat)
1407    case _:
1408      Invalid{0n}
1409
1410# Whether s ends in "...": run counts the dots just before s.
1411def ends_in_ellipsis(s: String, run: Nat) -> Bool:
1412  match s:
1413    case SNil{}: Nat.is_lt(2n, run)
1414    case SCon{h, t}:
1415      ends_in_ellipsis(t, Bool.pick(Nat, Char.is_eq(h, '.'), Nat.add(run, 1n), 0n))
1416
1417def parse_usage_repeated(repeated: Bool, +usage: String) -> Usage:
1418  match repeated:
1419    case False{}: parse_usage_body(usage, False{})
1420    case True{}:
1421      parse_usage_body(String.take(usage, Nat.sub(String.length(usage), 3n)),
1422        True{})
1423
1424def parse_usage(+usage: String) -> Usage:
1425  parse_usage_repeated(ends_in_ellipsis(usage, 0n), usage)
1426
1427def alias_valid_result(parsed: Usage, +raw: String) -> Bool:
1428  match parsed:
1429    case Parsed{id, short, long, FlagKind{}}:
1430      Bool.or(String.eq(raw, short), String.eq(raw, long))
1431    case _: False{}
1432
1433def valid_arg_alias(+raw: String) -> Bool:
1434  alias_valid_result(parse_usage(raw), raw)
1435
1436def kind_pos(kind: UsageKind) -> Bool:
1437  match kind:
1438    case PosKind{required, many}: True{}
1439    case _: False{}
1440
1441def kind_optional_pos(kind: UsageKind) -> Bool:
1442  match kind:
1443    case PosKind{required, many}: Bool.not(required)
1444    case _: False{}
1445
1446def kind_variadic_pos(kind: UsageKind) -> Bool:
1447  match kind:
1448    case PosKind{required, many}: many
1449    case _: False{}
1450
1451def kind_required_pos(kind: UsageKind) -> Bool:
1452  match kind:
1453    case PosKind{required, many}: required
1454    case _: False{}
1455
1456def kind_valued(kind: UsageKind) -> Bool:
1457  match kind:
1458    case OptionKind{many}: True{}
1459    case BareOptionKind{}: True{}
1460    case PosKind{required, many}: True{}
1461    case _: False{}
1462
1463def kind_bare(kind: UsageKind) -> Bool:
1464  match kind:
1465    case BareOptionKind{}: True{}
1466    case _: False{}
1467
1468def kind_default_allowed(kind: UsageKind) -> Bool:
1469  match kind:
1470    case OptionKind{many}: True{}
1471    case BareOptionKind{}: True{}
1472    case PosKind{required, many}: Bool.not(required)
1473    case NegatableKind{}: True{}
1474    case _: False{}
1475
1476def kind_required_allowed(kind: UsageKind) -> Bool:
1477  match kind:
1478    case OptionKind{many}: True{}
1479    case _: False{}
1480
1481def kind_delim_allowed(kind: UsageKind) -> Bool:
1482  match kind:
1483    case OptionKind{many}: many
1484    case PosKind{required, many}: many
1485    case _: False{}
1486
1487def bad_attr(cond: Bool, name: String) -> Maybe<&2, String>:
1488  Bool.pick(Maybe<&2, String>, cond, Some{name}, None{})
1489
1490def misplaced_attr_name(attr: Item, +kind: UsageKind) ->
1491    Maybe<&2, String>:
1492  match attr:
1493    case Global{}: bad_attr(kind_pos(kind), "global")
1494    case Rest{}: bad_attr(Bool.not(kind_variadic_pos(kind)), "rest")
1495    case AttrDefault{value}:
1496      bad_attr(Bool.not(kind_default_allowed(kind)), "default")
1497    case Required{}:
1498      bad_attr(Bool.not(kind_required_allowed(kind)), "required")
1499    case Heading{title}: bad_attr(kind_pos(kind), "heading")
1500    case Alias{name}: bad_attr(kind_pos(kind), "alias")
1501    case As{t}: bad_attr(Bool.not(kind_valued(kind)), type_name(t))
1502    case OneOf{values}:
1503      bad_attr(Bool.not(kind_valued(kind)), "one_of")
1504    case Delim{sep}: bad_attr(Bool.not(kind_delim_allowed(kind)), "delim")
1505    case Hyphen{}: bad_attr(Bool.not(kind_valued(kind)), "hyphen")
1506    case Arg{usage, help, items}: Some{"arg"}
1507    case Sub{name, about, items}: Some{"sub"}
1508    case Version{value}: Some{"version"}
1509    case Example{line, what}: Some{"example"}
1510    case After{text}: Some{"after"}
1511    case SubOptional{}: Some{"sub_optional"}
1512    case External{}: Some{"external"}
1513    case Exclusive{group}: Some{"exclusive"}
1514    case NeedOne{group}: Some{"need_one"}
1515    case _: None{}
1516
1517def maybe_misplaced(found: Maybe<&2, String>, at: String) ->
1518    List<&2, Problem>:
1519  match found:
1520    case None{}: Nil{}
1521    case Some{name}: [Misplaced{at, name}]
1522
1523def placement_issues(attrs: List<&2, Item>, +kind: UsageKind,
1524    +at: String) -> List<&2, Problem>:
1525  match attrs:
1526    case Nil{}: Nil{}
1527    case head <> tail:
1528      List.append(&2, Problem,
1529        maybe_misplaced(misplaced_attr_name(head, kind), at),
1530        placement_issues(tail, kind, at))
1531
1532def value_type_names(attrs: List<&2, Item>) -> List<&2, String>:
1533  match attrs:
1534    case Nil{}: Nil{}
1535    case As{t} <> tail: type_name(t) <> value_type_names(tail)
1536    case head <> tail: value_type_names(tail)
1537
1538def extra_type_issues(names: List<&2, String>, +at: String) ->
1539    List<&2, Problem>:
1540  match names:
1541    case Nil{}: Nil{}
1542    case name <> tail:
1543      Misplaced{at, name} <> extra_type_issues(tail, at)
1544
1545def duplicate_type_issues(names: List<&2, String>, at: String) ->
1546    List<&2, Problem>:
1547  match names:
1548    case Nil{}: Nil{}
1549    case head <> tail: extra_type_issues(tail, at)
1550
1551def command_attr_name(attr: Item, root: Bool) -> Maybe<&2, String>:
1552  match attr:
1553    case Arg{usage, help, attrs}: None{}
1554    case Sub{name, about, children}: None{}
1555    case Version{value}: bad_attr(Bool.not(root), "version")
1556    case Alias{name}: bad_attr(root, "alias")
1557    case Hidden{}: bad_attr(root, "hidden")
1558    case Example{line, what}: None{}
1559    case After{text}: None{}
1560    case SubOptional{}: None{}
1561    case External{}: None{}
1562    case Exclusive{group}: None{}
1563    case NeedOne{group}: None{}
1564    case Id{value}: Some{"id"}
1565    case Required{}: Some{"required"}
1566    case AttrDefault{value}: Some{"default"}
1567    case AttrEnv{name}: Some{"env"}
1568    case Global{}: Some{"global"}
1569    case Heading{title}: Some{"heading"}
1570    case As{t}: Some{type_name(t)}
1571    case OneOf{values}: Some{"one_of"}
1572    case Delim{sep}: Some{"delim"}
1573    case Hyphen{}: Some{"hyphen"}
1574    case Rest{}: Some{"rest"}
1575    case Bare{value}: Some{"bare"}
1576    case Conflicts{id}: Some{"conflicts"}
1577    case Requires{id}: Some{"requires"}
1578    case Group{name}: Some{"group"}
1579    case Builtin{}: None{}
1580
1581def has_rest(items: List<&2, Item>) -> Bool:
1582  match items:
1583    case Nil{}: False{}
1584    case Rest{} <> tail: True{}
1585    case head <> tail: has_rest(tail)
1586
1587def has_hidden(items: List<&2, Item>) -> Bool:
1588  match items:
1589    case Nil{}: False{}
1590    case Hidden{} <> tail: True{}
1591    case head <> tail: has_hidden(tail)
1592
1593def has_builtin(items: List<&2, Item>) -> Bool:
1594  match items:
1595    case Nil{}: False{}
1596    case Builtin{} <> tail: True{}
1597    case head <> tail: has_builtin(tail)
1598
1599def has_bare(items: List<&2, Item>) -> Bool:
1600  match items:
1601    case Nil{}: False{}
1602    case Bare{value} <> tail: True{}
1603    case head <> tail: has_bare(tail)
1604
1605def has_required(items: List<&2, Item>) -> Bool:
1606  match items:
1607    case Nil{}: False{}
1608    case Required{} <> tail: True{}
1609    case head <> tail: has_required(tail)
1610
1611def duplicate_strings(xs: List<&2, String>) -> Bool:
1612  match xs:
1613    case Nil{}: False{}
1614    case +head <> +tail:
1615      Bool.or(List.contains(~String, ~String.eq, tail, head),
1616        duplicate_strings(tail))
1617
1618def bad_choices(items: List<&2, Item>) -> Bool:
1619  match items:
1620    case Nil{}: False{}
1621    case OneOf{+values} <> tail:
1622      Bool.or(Bool.or(List.is_empty(&2, String, values),
1623        duplicate_strings(values)), bad_choices(tail))
1624    case head <> tail: bad_choices(tail)
1625
1626def find_default(items: List<&2, Item>) -> Maybe<&2, String>:
1627  match items:
1628    case Nil{}: None{}
1629    case AttrDefault{value} <> tail: Some{value}
1630    case head <> tail: find_default(tail)
1631
1632def find_delim(attrs: List<&2, Item>) -> Maybe<&2, Char>:
1633  match attrs:
1634    case Nil{}: None{}
1635    case Delim{sep} <> tail: Some{sep}
1636    case head <> tail: find_delim(tail)
1637
1638def split_each(values: List<&2, String>, +sep: Char) ->
1639    List<&2, String>:
1640  match values:
1641    case Nil{}: Nil{}
1642    case head <> tail:
1643      List.append(&2, String, String.split(head, sep),
1644        split_each(tail, sep))
1645
1646def apply_delim(found: Maybe<&2, Char>,
1647    values: List<&2, String>) -> List<&2, String>:
1648  match found:
1649    case None{}: values
1650    case Some{sep}: split_each(values, sep)
1651
1652def find_id(items: List<&2, Item>) -> Maybe<&2, String>:
1653  match items:
1654    case Nil{}: None{}
1655    case Id{value} <> tail: Some{value}
1656    case head <> tail: find_id(tail)
1657
1658def find_requires(items: List<&2, Item>) -> List<&2, String>:
1659  match items:
1660    case Nil{}: Nil{}
1661    case Requires{target} <> tail: target <> find_requires(tail)
1662    case head <> tail: find_requires(tail)
1663
1664def find_conflicts(items: List<&2, Item>) -> List<&2, String>:
1665  match items:
1666    case Nil{}: Nil{}
1667    case Conflicts{target} <> tail: target <> find_conflicts(tail)
1668    case head <> tail: find_conflicts(tail)
1669
1670def find_group_attrs(items: List<&2, Item>) -> List<&2, String>:
1671  match items:
1672    case Nil{}: Nil{}
1673    case Group{name} <> tail: name <> find_group_attrs(tail)
1674    case head <> tail: find_group_attrs(tail)
1675
1676def find_aliases(items: List<&2, Item>) -> List<&2, String>:
1677  match items:
1678    case Nil{}: Nil{}
1679    case Alias{name} <> tail: name <> find_aliases(tail)
1680    case head <> tail: find_aliases(tail)
1681
1682def scope_groups(items: List<&2, Item>) -> List<&2, String>:
1683  match items:
1684    case Nil{}: Nil{}
1685    case Arg{usage, help, attrs} <> tail:
1686      List.append(&2, String, find_group_attrs(attrs), scope_groups(tail))
1687    case head <> tail: scope_groups(tail)
1688
1689def scope_group_rules(items: List<&2, Item>) -> List<&2, String>:
1690  match items:
1691    case Nil{}: Nil{}
1692    case Exclusive{group} <> tail: group <> scope_group_rules(tail)
1693    case NeedOne{group} <> tail: group <> scope_group_rules(tail)
1694    case head <> tail: scope_group_rules(tail)
1695
1696def usage_kind(~f: UsageKind -> Bool, parsed: Usage) -> Bool:
1697  match parsed:
1698    case Parsed{id, short, long, kind}: f(kind)
1699    case Invalid{col}: False{}
1700
1701# An item as lint walks it: an Arg with its usage parsed once, a Sub with
1702# its children's too.
1703type LintItem is Data:
1704  LintArg{usage: String, help: String, attrs: List<&2, Item>, parsed: Usage}
1705  LintSub{name: String, about: String, children: List<&2, Item>,
1706    items: List<&2, LintItem>}
1707  LintOther{item: Item}
1708
1709def lint_items(items: List<&2, Item>) -> List<&2, LintItem>:
1710  match items:
1711    case Nil{}: Nil{}
1712    case Arg{+usage, help, attrs} <> tail:
1713      LintArg{usage, help, attrs, parse_usage(usage)} <> lint_items(tail)
1714    case Sub{name, about, +children} <> tail:
1715      LintSub{name, about, children, lint_items(children)} <> lint_items(tail)
1716    case head <> tail: LintOther{head} <> lint_items(tail)
1717
1718def has_pos_item(items: List<&2, LintItem>) -> Bool:
1719  match items:
1720    case Nil{}: False{}
1721    case LintArg{usage, help, attrs, parsed} <> tail:
1722      Bool.or(usage_kind(~kind_pos, parsed), has_pos_item(tail))
1723    case head <> tail: has_pos_item(tail)
1724
1725def has_sub_item(items: List<&2, Item>) -> Bool:
1726  match items:
1727    case Nil{}: False{}
1728    case Sub{name, about, children} <> tail: True{}
1729    case head <> tail: has_sub_item(tail)
1730
1731def has_version_item(items: List<&2, Item>) -> Bool:
1732  match items:
1733    case Nil{}: False{}
1734    case Version{value} <> tail: True{}
1735    case head <> tail: has_version_item(tail)
1736
1737def name_scan_valid(result: NameScan) -> Bool:
1738  match result:
1739    case NameOK{name, SNil{}, next}: True{}
1740    case _: False{}
1741
1742def valid_command_name(s: String) -> Bool:
1743  name_scan_valid(scan_name(s, 0n, "", True{}, 0n))
1744
1745def use_id(found: Maybe<&2, String>, derived: String) -> String:
1746  match found:
1747    case None{}: derived
1748    case Some{explicit}: explicit
1749
1750def usage_id(parsed: Usage, attrs: List<&2, Item>) -> String:
1751  match parsed:
1752    case Parsed{derived, short, long, kind}:
1753      use_id(find_id(attrs), derived)
1754    case Invalid{col}:
1755      ""
1756
1757def scope_ids(items: List<&2, LintItem>) -> List<&2, String>:
1758  match items:
1759    case Nil{}:
1760      Nil{}
1761    case LintArg{usage, help, attrs, parsed} <> tail:
1762      +name = usage_id(parsed, attrs)
1763      +rest = scope_ids(tail)
1764      Bool.pick(List<&2, String>, String.is_empty(name),
1765        rest, name <> rest)
1766    case head <> tail:
1767      scope_ids(tail)
1768
1769type Seen is Data:
1770  SeenArg{id: String, short: String, long: String}
1771  SeenSub{name: String}
1772  SeenArgAlias{name: String}
1773  SeenSubAlias{name: String}
1774
1775# String.eq, without rebuilding both strings to hand them back; the walk
1776# stops at the first difference.
1777def same_go(a: String, b: String, same: Bool) -> Bool:
1778  match a b same:
1779    case SNil{} SNil{} done: done
1780    case SCon{x, xt} SCon{y, yt} True{}: same_go(xt, yt, Char.is_eq(x, y))
1781    case _ _ _: False{}
1782
1783def same_text(a: String, b: String) -> Bool:
1784  same_go(a, b, True{})
1785
1786def same_nonempty(+a: String, b: String) -> Bool:
1787  Bool.and(Bool.not(String.is_empty(a)), same_text(a, b))
1788
1789def arg_collision(seen: List<&2, Seen>, +id: String,
1790    +short: String, +long: String) -> Maybe<&2, String>:
1791  match seen:
1792    case Nil{}:
1793      None{}
1794    case SeenArg{old_id, old_short, old_long} <> tail:
1795      here = Bool.pick(Maybe<&2, String>, same_nonempty(short, old_short),
1796        Some{short}, Bool.pick(Maybe<&2, String>,
1797          same_nonempty(long, old_long), Some{long},
1798          Bool.pick(Maybe<&2, String>, same_text(id, old_id), Some{id}, None{})))
1799      Maybe.or(&2, String, here, arg_collision(tail, id, short, long))
1800    case SeenArgAlias{+old} <> tail:
1801      Bool.pick(Maybe<&2, String>, same_nonempty(short, old),
1802        Some{short}, Bool.pick(Maybe<&2, String>,
1803          same_nonempty(long, old), Some{long},
1804          arg_collision(tail, id, short, long)))
1805    case head <> tail:
1806      arg_collision(tail, id, short, long)
1807
1808def sub_collision(seen: List<&2, Seen>, +name: String) -> Bool:
1809  match seen:
1810    case Nil{}:
1811      False{}
1812    case SeenSub{old} <> tail:
1813      Bool.or(same_text(name, old), sub_collision(tail, name))
1814    case SeenSubAlias{old} <> tail:
1815      Bool.or(same_text(name, old), sub_collision(tail, name))
1816    case head <> tail:
1817      sub_collision(tail, name)
1818
1819def arg_alias_collision(seen: List<&2, Seen>, +name: String) -> Bool:
1820  match seen:
1821    case Nil{}: False{}
1822    case SeenArg{id, short, long} <> tail:
1823      Bool.or(Bool.or(same_nonempty(name, short),
1824        same_nonempty(name, long)), arg_alias_collision(tail, name))
1825    case SeenArgAlias{old} <> tail:
1826      Bool.or(same_text(name, old), arg_alias_collision(tail, name))
1827    case head <> tail:
1828      arg_alias_collision(tail, name)
1829
1830def maybe_dup(found: Maybe<&2, String>, at: String) -> List<&2, Problem>:
1831  match found:
1832    case None{}: Nil{}
1833    case Some{name}: [Dup{at, name}]
1834
1835def when_problem(cond: Bool, problem: Problem) -> List<&2, Problem>:
1836  Bool.pick(List<&2, Problem>, cond, [problem], Nil{})
1837
1838def arg_alias_issue(+name: String, +at: String,
1839    seen: List<&2, Seen>, version: Bool) -> List<&2, Problem>:
1840  Bool.pick(List<&2, Problem>, valid_arg_alias(name),
1841    Bool.pick(List<&2, Problem>, Bool.or(String.eq(name, "--help"),
1842      Bool.and(version, String.eq(name, "--version"))),
1843      [Reserved{at, name}],
1844      when_problem(arg_alias_collision(seen, name), Dup{at, name})),
1845    [BadName{at, name}])
1846
1847def arg_alias_issues(aliases: List<&2, String>, +at: String,
1848    +seen: List<&2, Seen>, +version: Bool) -> List<&2, Problem>:
1849  match aliases:
1850    case Nil{}: Nil{}
1851    case +name <> tail:
1852      List.append(&2, Problem,
1853        arg_alias_issue(name, at, seen, version),
1854        arg_alias_issues(tail, at, SeenArgAlias{name} <> seen, version))
1855
1856def arg_alias_entries(aliases: List<&2, String>) -> List<&2, Seen>:
1857  match aliases:
1858    case Nil{}: Nil{}
1859    case name <> tail:
1860      SeenArgAlias{name} <> arg_alias_entries(tail)
1861
1862def arg_entries(parsed: Usage, +attrs: List<&2, Item>) -> List<&2, Seen>:
1863  match parsed:
1864    case Parsed{derived, short, long, kind}:
1865      SeenArg{use_id(find_id(attrs), derived), short, long} <>
1866        arg_alias_entries(find_aliases(attrs))
1867    case Invalid{col}:
1868      Nil{}
1869
1870def has_global(items: List<&2, Item>) -> Bool:
1871  match items:
1872    case Nil{}: False{}
1873    case Global{} <> tail: True{}
1874    case head <> tail: has_global(tail)
1875
1876def global_entries(items: List<&2, LintItem>) -> List<&2, Seen>:
1877  match items:
1878    case Nil{}: Nil{}
1879    case LintArg{usage, help, +attrs, parsed} <> tail:
1880      List.append(&2, Seen,
1881        Bool.pick(List<&2, Seen>, has_global(attrs),
1882          arg_entries(parsed, attrs), Nil{}),
1883        global_entries(tail))
1884    case head <> tail: global_entries(tail)
1885
1886def sub_alias_issue(+name: String, +at: String,
1887    seen: List<&2, Seen>) -> List<&2, Problem>:
1888  Bool.pick(List<&2, Problem>, valid_command_name(name),
1889    Bool.pick(List<&2, Problem>, String.eq(name, "help"),
1890      [Reserved{at, name}],
1891      when_problem(sub_collision(seen, name), Dup{at, name})),
1892    [BadName{at, name}])
1893
1894def sub_alias_issues(aliases: List<&2, String>, +at: String,
1895    +seen: List<&2, Seen>) -> List<&2, Problem>:
1896  match aliases:
1897    case Nil{}: Nil{}
1898    case +name <> tail:
1899      List.append(&2, Problem,
1900        sub_alias_issue(name, at, seen),
1901        sub_alias_issues(tail, at, SeenSubAlias{name} <> seen))
1902
1903def sub_alias_entries(aliases: List<&2, String>) -> List<&2, Seen>:
1904  match aliases:
1905    case Nil{}: Nil{}
1906    case name <> tail:
1907      SeenSubAlias{name} <> sub_alias_entries(tail)
1908
1909def missing_refs(targets: List<&2, String>, +ids: List<&2, String>,
1910    +at: String) -> List<&2, Problem>:
1911  match targets:
1912    case Nil{}:
1913      Nil{}
1914    case +target <> tail:
1915      List.append(&2, Problem,
1916        when_problem(Bool.not(List.contains(~String, ~String.eq, ids, target)),
1917          BadRef{at, target}), missing_refs(tail, ids, at))
1918
1919# A U32 value is what U32.read reads (P22).
1920def valid_u32(s: String) -> Bool:
1921  Maybe.is_some(&2, U32, U32.read(s))
1922
1923# Decimal digits by Nat.read's own test, which Char.is_digit agrees with.
1924def decimal_digits(s: String) -> Bool:
1925  match s:
1926    case SNil{}: True{}
1927    case SCon{Chr{x}, tail}:
1928      Bool.and(U32.is_lt(U32.sub(x, 48), 10), decimal_digits(tail))
1929
1930# A Nat value is what Nat.read reads (P22): digits that, less leading
1931# zeroes, fit under its cap of 2^48-1.
1932def valid_nat(+s: String) -> Bool:
1933  Bool.and(Bool.not(String.is_empty(s)),
1934    Bool.and(decimal_digits(s), Nat.read.fits(Nat.read.trim(s))))
1935
1936def valid_bool(s: String) -> Bool:
1937  List.contains(~String, ~String.eq,
1938    ["true", "false", "yes", "no", "on", "off", "1", "0"],
1939    String.to_lower(s))
1940
1941type DecimalState is Data:
1942  DecStart{}
1943  DecAfterMinus{}
1944  DecInt{}
1945  DecDotFirst{}
1946  DecDotAfterInt{}
1947  DecFrac{}
1948  DecExpStart{}
1949  DecExpSign{}
1950  DecExpDigits{}
1951  DecInvalid{}
1952
1953def is_exponent(+c: Char) -> Bool:
1954  Bool.or(Char.is_eq(c, 'e'), Char.is_eq(c, 'E'))
1955
1956def is_sign(+c: Char) -> Bool:
1957  Bool.or(Char.is_eq(c, '+'), Char.is_eq(c, '-'))
1958
1959def decimal_step(state: DecimalState, +c: Char) -> DecimalState:
1960  match state:
1961    case DecStart{}:
1962      Bool.pick(DecimalState, Char.is_eq(c, '-'), DecAfterMinus{},
1963        Bool.pick(DecimalState, Char.is_digit(c), DecInt{},
1964          Bool.pick(DecimalState, Char.is_eq(c, '.'), DecDotFirst{}, DecInvalid{})))
1965    case DecAfterMinus{}:
1966      Bool.pick(DecimalState, Char.is_digit(c), DecInt{},
1967        Bool.pick(DecimalState, Char.is_eq(c, '.'), DecDotFirst{}, DecInvalid{}))
1968    case DecInt{}:
1969      Bool.pick(DecimalState, Char.is_digit(c), DecInt{},
1970        Bool.pick(DecimalState, Char.is_eq(c, '.'), DecDotAfterInt{},
1971          Bool.pick(DecimalState, is_exponent(c), DecExpStart{}, DecInvalid{})))
1972    case DecDotFirst{}:
1973      Bool.pick(DecimalState, Char.is_digit(c), DecFrac{}, DecInvalid{})
1974    case DecDotAfterInt{}:
1975      Bool.pick(DecimalState, Char.is_digit(c), DecFrac{},
1976        Bool.pick(DecimalState, is_exponent(c), DecExpStart{}, DecInvalid{}))
1977    case DecFrac{}:
1978      Bool.pick(DecimalState, Char.is_digit(c), DecFrac{},
1979        Bool.pick(DecimalState, is_exponent(c), DecExpStart{}, DecInvalid{}))
1980    case DecExpStart{}:
1981      Bool.pick(DecimalState, is_sign(c), DecExpSign{},
1982        Bool.pick(DecimalState, Char.is_digit(c), DecExpDigits{}, DecInvalid{}))
1983    case DecExpSign{}:
1984      Bool.pick(DecimalState, Char.is_digit(c), DecExpDigits{}, DecInvalid{})
1985    case DecExpDigits{}:
1986      Bool.pick(DecimalState, Char.is_digit(c), DecExpDigits{}, DecInvalid{})
1987    case DecInvalid{}:
1988      DecInvalid{}
1989
1990def decimal_accept(state: DecimalState) -> Bool:
1991  match state:
1992    case DecInt{}: True{}
1993    case DecDotAfterInt{}: True{}
1994    case DecFrac{}: True{}
1995    case DecExpDigits{}: True{}
1996    case _: False{}
1997
1998def decimal_scan(s: String, state: DecimalState) -> Bool:
1999  match s:
2000    case SNil{}: decimal_accept(state)
2001    case SCon{head, tail}:
2002      decimal_scan(tail, decimal_step(state, head))
2003
2004def valid_f32(s: String) -> Bool:
2005  decimal_scan(s, DecStart{})
2006
2007def find_one_of(items: List<&2, Item>) -> Maybe<&2, List<&2, String>>:
2008  match items:
2009    case Nil{}: None{}
2010    case OneOf{values} <> tail: Some{values}
2011    case head <> tail: find_one_of(tail)
2012
2013# The first element of a value that fails to convert, and why.
2014type Bad is Data:
2015  Bad{value: String, reason: String}
2016
2017def find_as(attrs: List<&2, Item>) -> Maybe<&2, ValueType>:
2018  match attrs:
2019    case Nil{}: None{}
2020    case As{kind} <> tail: Some{kind}
2021    case head <> tail: find_as(tail)
2022
2023def type_reason(kind: ValueType, value: String) -> Maybe<&2, String>:
2024  match kind:
2025    case TNat{}:
2026      Bool.pick(Maybe<&2, String>, valid_nat(value), None{},
2027        Some{"expected a whole number from 0 to 281474976710655"})
2028    case TU32{}:
2029      Bool.pick(Maybe<&2, String>, valid_u32(value), None{},
2030        Some{"expected a whole number from 0 to 4294967295"})
2031    case TF32{}:
2032      Bool.pick(Maybe<&2, String>, valid_f32(value), None{},
2033        Some{"expected a decimal number"})
2034    case TBool{}:
2035      Bool.pick(Maybe<&2, String>, valid_bool(value), None{},
2036        Some{"expected true or false"})
2037    case TFile{}: None{}
2038    case TDir{}: None{}
2039
2040def quoted_choices(values: List<&2, String>) -> List<&2, String>:
2041  match values:
2042    case Nil{}: Nil{}
2043    case head <> tail:
2044      String.concat(["'", head, "'"]) <> quoted_choices(tail)
2045
2046def choices_reason(values: List<&2, String>) -> String:
2047  String.append("expected one of ",
2048    String.join(quoted_choices(values), ", "))
2049
2050def choice_reason(found: Maybe<&2, List<&2, String>>,
2051    value: String) -> Maybe<&2, String>:
2052  match found:
2053    case None{}: None{}
2054    case Some{+values}:
2055      Bool.pick(Maybe<&2, String>,
2056        List.contains(~String, ~String.eq, values, value),
2057        None{}, Some{choices_reason(values)})
2058
2059def reason_from_type(found: Maybe<&2, ValueType>,
2060    value: String) -> Maybe<&2, String>:
2061  match found:
2062    case None{}: None{}
2063    case Some{kind}: type_reason(kind, value)
2064
2065def first_reason(found: Maybe<&2, String>,
2066    other: Maybe<&2, String>) -> Maybe<&2, String>:
2067  match found:
2068    case None{}: other
2069    case Some{reason}: Some{reason}
2070
2071def value_reason(+attrs: List<&2, Item>, +value: String) ->
2072    Maybe<&2, String>:
2073  first_reason(reason_from_type(find_as(attrs), value),
2074    choice_reason(find_one_of(attrs), value))
2075
2076def bad_value(reason: Maybe<&2, String>, value: String) ->
2077    Maybe<&2, Bad>:
2078  match reason:
2079    case None{}: None{}
2080    case Some{why}: Some{Bad{value, why}}
2081
2082def first_bad(values: List<&2, String>, +attrs: List<&2, Item>) ->
2083    Maybe<&2, Bad>:
2084  match values:
2085    case Nil{}: None{}
2086    case +head <> tail:
2087      Maybe.or(&2, Bad, bad_value(value_reason(attrs, head), head),
2088        first_bad(tail, attrs))
2089
2090def split_bad(found: Maybe<&2, Char>, attrs: List<&2, Item>,
2091    +value: String) -> Maybe<&2, Bad>:
2092  match found:
2093    case None{}: bad_value(value_reason(attrs, value), value)
2094    case Some{sep}: first_bad(String.split(value, sep), attrs)
2095
2096# A delimited value splits first; the first element that fails (§8.2).
2097def value_bad(+attrs: List<&2, Item>, value: String) -> Maybe<&2, Bad>:
2098  split_bad(find_delim(attrs), attrs, value)
2099
2100# A value from env or a default converts each element on its own (§8.2).
2101def value_issue(kind: UsageKind, attrs: List<&2, Item>,
2102    +value: String) -> Maybe<&2, Bad>:
2103  match kind:
2104    case FlagKind{}: bad_value(type_reason(TBool{}, value), value)
2105    case NegatableKind{}: bad_value(type_reason(TBool{}, value), value)
2106    case CountKind{}: bad_value(type_reason(TNat{}, value), value)
2107    case _: value_bad(attrs, value)
2108
2109def bad_default(required: Bool, issue: Maybe<&2, Bad>, value: String,
2110    id: String, at: String) -> List<&2, Problem>:
2111  match required issue:
2112    case True{} _: [BadDefault{at, id, value}]
2113    case False{} Some{Bad{bad, why}}: [BadDefault{at, id, bad}]
2114    case False{} None{}: Nil{}
2115
2116def check_default(found: Maybe<&2, String>, kind: UsageKind,
2117    +attrs: List<&2, Item>, id: String, at: String) -> List<&2, Problem>:
2118  match found:
2119    case None{}:
2120      Nil{}
2121    case Some{+value}:
2122      bad_default(has_required(attrs), value_issue(kind, attrs, value),
2123        value, id, at)
2124
2125# The first choice the arg's type rejects; after a hit it only walks the rest.
2126def choice_bad(values: List<&2, String>, +found: Maybe<&2, ValueType>,
2127    bad: Maybe<&2, Bad>) -> Maybe<&2, Bad>:
2128  match values bad:
2129    case Nil{} hit: hit
2130    case value <> tail Some{hit}: Some{hit}
2131    case +value <> tail None{}:
2132      choice_bad(tail, found, bad_value(reason_from_type(found, value), value))
2133
2134def bad_choice(found: Maybe<&2, Bad>, id: String, at: String) ->
2135    List<&2, Problem>:
2136  match found:
2137    case None{}: Nil{}
2138    case Some{Bad{value, why}}: [BadChoice{at, id, value}]
2139
2140# Parse checks a value's type before its choices, so a choice the type
2141# rejects could never be given.
2142def check_choices(found: Maybe<&2, List<&2, String>>,
2143    kind: Maybe<&2, ValueType>, id: String, at: String) -> List<&2, Problem>:
2144  match found:
2145    case None{}: Nil{}
2146    case Some{values}: bad_choice(choice_bad(values, kind, None{}), id, at)
2147
2148def positional_issue(+kind: UsageKind, +id: String,
2149    attrs: List<&2, Item>, rest_items: List<&2, LintItem>,
2150    +seen_optional: Bool, +seen_variadic: Bool, +at: String) ->
2151    List<&2, Problem>:
2152  Bool.pick(List<&2, Problem>,
2153    Bool.and(seen_optional, kind_required_pos(kind)),
2154    [BadPositional{at, id, "required positional follows an optional positional"}],
2155    Bool.pick(List<&2, Problem>,
2156      Bool.and(seen_variadic, kind_variadic_pos(kind)),
2157      [BadPositional{at, id, "more than one variadic positional"}],
2158      Bool.pick(List<&2, Problem>,
2159        Bool.and(seen_variadic, kind_optional_pos(kind)),
2160        [BadPositional{at, id, "optional positional follows a variadic positional"}],
2161        when_problem(Bool.and(kind_variadic_pos(kind),
2162          Bool.and(has_rest(attrs), has_pos_item(rest_items))),
2163          BadPositional{at, id, "rest must be on the last positional"}))))
2164
2165def bare_issue(+kind: UsageKind, id: String, short: String,
2166    +attrs: List<&2, Item>, at: String) -> List<&2, Problem>:
2167  +bare_kind = kind_bare(kind)
2168  +has = has_bare(attrs)
2169  when_problem(Bool.or(
2170    Bool.and(bare_kind, Bool.or(Bool.not(has),
2171      Bool.not(String.is_empty(short)))),
2172    Bool.and(Bool.not(bare_kind), has)), NeedBare{at, id})
2173
2174def misplaced_arg(+kind: UsageKind, +attrs: List<&2, Item>,
2175    +at: String) -> List<&2, Problem>:
2176  List.append(&2, Problem, placement_issues(attrs, kind, at),
2177    duplicate_type_issues(value_type_names(attrs), at))
2178
2179def arg_reserved_dup(+at: String, +long: String, seen: List<&2, Seen>,
2180    id: String, short: String, version: Bool) -> List<&2, Problem>:
2181  reserved = Bool.or(String.eq(long, "--help"),
2182    Bool.and(version, String.eq(long, "--version")))
2183  Bool.pick(List<&2, Problem>, reserved,
2184    [Reserved{at, long}], maybe_dup(arg_collision(seen, id, short, long), at))
2185
2186def arg_issues(+kind: UsageKind, +id: String, +short: String,
2187    +long: String, usage: String, help: String, +attrs: List<&2, Item>,
2188    remaining: List<&2, LintItem>, +at: String, ids: List<&2, String>,
2189    seen: List<&2, Seen>, optional: Bool, variadic: Bool,
2190    version: Bool) -> List<&2, Problem>:
2191  List.concat(&2, Problem, [
2192    arg_reserved_dup(at, long, seen, id, short, version),
2193    misplaced_arg(kind, attrs, at),
2194    when_problem(Bool.and(String.is_empty(help),
2195      Bool.not(has_hidden(attrs))), NoHelp{at, usage}),
2196    check_default(find_default(attrs), kind, attrs, id, at),
2197    missing_refs(List.append(&2, String,
2198      find_requires(attrs), find_conflicts(attrs)), ids, at),
2199    positional_issue(kind, id, attrs, remaining, optional, variadic, at),
2200    when_problem(bad_choices(attrs), BadChoices{at, id}),
2201    check_choices(find_one_of(attrs), find_as(attrs), id, at),
2202    bare_issue(kind, id, short, attrs, at),
2203  ])
2204
2205def arg_problems(parsed: Usage, +usage: String, help: String,
2206    +attrs: List<&2, Item>, remaining: List<&2, LintItem>, +at: String,
2207    ids: List<&2, String>, +seen: List<&2, Seen>,
2208    optional: Bool, variadic: Bool, +version: Bool) -> List<&2, Problem>:
2209  match parsed:
2210    case Invalid{col}:
2211      List.append(&2, Problem, [BadUsage{at, usage, col}],
2212        when_problem(Bool.and(String.is_empty(help),
2213          Bool.not(has_hidden(attrs))), NoHelp{at, usage}))
2214    case Parsed{derived, +short, +long, +kind}:
2215      +id = use_id(find_id(attrs), derived)
2216      +aliases = find_aliases(attrs)
2217      List.append(&2, Problem,
2218        arg_issues(kind, id, short, long, usage, help, attrs,
2219          remaining, at, ids, seen, optional, variadic, version),
2220        arg_alias_issues(aliases, at, SeenArg{id, short, long} <> seen,
2221          version))
2222
2223def has_external(items: List<&2, Item>) -> Bool:
2224  match items:
2225    case Nil{}: False{}
2226    case External{} <> tail: True{}
2227    case head <> tail: has_external(tail)
2228
2229def has_sub_optional(items: List<&2, Item>) -> Bool:
2230  match items:
2231    case Nil{}: False{}
2232    case SubOptional{} <> tail: True{}
2233    case head <> tail: has_sub_optional(tail)
2234
2235# A command that cannot complete without a subcommand (§8.4).
2236def needs_sub(+items: List<&2, Item>) -> Bool:
2237  Bool.and(has_sub_item(items),
2238    Bool.and(Bool.not(has_sub_optional(items)),
2239      Bool.not(has_external(items))))
2240
2241# Each required() among attrs, as Misplaced at at.
2242def required_misplaced(attrs: List<&2, Item>, +at: String) ->
2243    List<&2, Problem>:
2244  match attrs:
2245    case Nil{}: Nil{}
2246    case Required{} <> tail:
2247      Misplaced{at, "required"} <> required_misplaced(tail, at)
2248    case head <> tail: required_misplaced(tail, at)
2249
2250def dead_required_option(option: Bool, attrs: List<&2, Item>, +at: String) ->
2251    List<&2, Problem>:
2252  match option:
2253    case True{}: required_misplaced(attrs, at)
2254    case False{}: Nil{}
2255
2256# An option's required() that descent drops; a flag's or a positional's is
2257# already misplaced by its kind.
2258def dead_required_kind(parsed: Usage, attrs: List<&2, Item>, +at: String) ->
2259    List<&2, Problem>:
2260  match parsed:
2261    case Parsed{derived, short, long, kind}:
2262      dead_required_option(kind_required_allowed(kind), attrs, at)
2263    case Invalid{col}: Nil{}
2264
2265def dead_required_arg(kept: Bool, usage: String, attrs: List<&2, Item>,
2266    +at: String) -> List<&2, Problem>:
2267  match kept:
2268    case True{}: Nil{}
2269    case False{}: dead_required_kind(parse_usage(usage), attrs, at)
2270
2271def dead_required_items(items: List<&2, Item>, +at: String) ->
2272    List<&2, Problem>:
2273  match items:
2274    case Nil{}: Nil{}
2275    case Arg{usage, help, +attrs} <> tail:
2276      List.append(&2, Problem,
2277        dead_required_arg(Bool.or(has_global(attrs),
2278          Bool.not(has_required(attrs))), usage, attrs, at),
2279        dead_required_items(tail, at))
2280    case head <> tail: dead_required_items(tail, at)
2281
2282# A command that needs a subcommand passes down only its global options'
2283# values (§8.4), so another option's required() can never be met.
2284def dead_required(dead: Bool, items: List<&2, Item>, +at: String) ->
2285    List<&2, Problem>:
2286  match dead:
2287    case True{}: dead_required_items(items, at)
2288    case False{}: Nil{}
2289
2290# has_pos: whether items has a positional.
2291def command_body_issues(+at: String, name: String, about: String,
2292    +items: List<&2, Item>, has_pos: Bool) -> List<&2, Problem>:
2293  List.concat(&2, Problem, [
2294    when_problem(Bool.and(String.is_empty(about),
2295      Bool.not(has_hidden(items))), NoHelp{at, name}),
2296    when_problem(Bool.and(has_pos, has_sub_item(items)), Mixed{at}),
2297    missing_refs(scope_group_rules(items), scope_groups(items), at),
2298    missing_refs(scope_groups(items), scope_group_rules(items), at),
2299    dead_required(needs_sub(items), items, at),
2300  ])
2301
2302def sub_name_issues(+at: String, +name: String,
2303    seen: List<&2, Seen>) -> List<&2, Problem>:
2304  List.append(&2, Problem,
2305    when_problem(Bool.not(valid_command_name(name)), BadName{at, name}),
2306    Bool.pick(List<&2, Problem>, String.eq(name, "help"),
2307      [Reserved{at, name}],
2308      when_problem(sub_collision(seen, name), Dup{at, name})))
2309
2310def lint(items: List<&2, LintItem>, +at: String, +ids: List<&2, String>,
2311    +seen: List<&2, Seen>, +inherited: List<&2, Seen>, +optional: Bool,
2312    +variadic: Bool, +version: Bool, +root: Bool) -> List<&2, Problem>:
2313  match items:
2314    case Nil{}:
2315      Nil{}
2316    case LintArg{+usage, help, +attrs, +parsed} <> +tail:
2317      List.append(&2, Problem,
2318        arg_problems(parsed, usage, help, attrs, tail, at, ids, seen,
2319          optional, variadic, version),
2320        lint(tail, at, ids,
2321          List.append(&2, Seen, arg_entries(parsed, attrs), seen),
2322          inherited,
2323          Bool.or(optional, usage_kind(~kind_optional_pos, parsed)),
2324          Bool.or(variadic, usage_kind(~kind_variadic_pos, parsed)),
2325          version, root))
2326    case LintSub{+name, about, +children, +kids} <> tail:
2327      +child_at = String.concat([at, " ", name])
2328      +aliases = find_aliases(children)
2329      List.concat(&2, Problem, [
2330        sub_name_issues(at, name, seen),
2331        sub_alias_issues(aliases, at, SeenSub{name} <> seen),
2332        command_body_issues(child_at, name, about, children,
2333          has_pos_item(kids)),
2334        lint(kids, child_at, scope_ids(kids), inherited,
2335          List.append(&2, Seen, global_entries(kids), inherited),
2336          False{}, False{}, version, False{}),
2337        lint(tail, at, ids,
2338          List.append(&2, Seen, sub_alias_entries(aliases),
2339            SeenSub{name} <> seen),
2340          inherited, optional, variadic, version, root),
2341      ])
2342    case LintOther{head} <> tail:
2343      List.append(&2, Problem,
2344        maybe_misplaced(command_attr_name(head, root), at),
2345        lint(tail, at, ids, seen, inherited, optional, variadic,
2346          version, root))
2347
2348def problems(c: Cmd) -> List<&2, Problem>:
2349  match c:
2350    case Cmd{+name, about, +items}:
2351      +linted = lint_items(items)
2352      List.concat(&2, Problem, [
2353        when_problem(Bool.not(valid_command_name(name)), BadName{name, name}),
2354        command_body_issues(name, name, about, items, has_pos_item(linted)),
2355        lint(linted, name, scope_ids(linted), Nil{}, global_entries(linted),
2356          False{}, False{}, has_version_item(items), True{}),
2357      ])
2358
2359type Src is Data:
2360  Argv{}
2361  Env{}
2362  Default{}
2363  Absent{}
2364
2365type Var is Data:
2366  Var{name: String, value: String}
2367
2368type Host is Data:
2369  Host{width: Nat, vars: List<&2, Var>, out_tty: Bool, err_tty: Bool}
2370
2371type Field is Data:
2372  Field{id: String, values: List<&2, String>, source: Src}
2373
2374type ScanEvent is Data:
2375  FlagHit{id: String, value: Bool}
2376  CountHit{id: String}
2377  ValueHit{id: String, value: String}
2378  Operand{token: String}
2379  Enter{name: String}
2380  HelpHit{long: Bool}
2381  VersionHit{}
2382
2383type ScanFault is Data:
2384  E01{token: String}
2385  E02{name: String}
2386  E03{arg: String}
2387  E04{arg: String, value: String, reason: String}
2388  E05{arg: String, value: String}
2389  E06{missing: List<&2, String>}
2390  E07{a: String, b: String}
2391  E08{a: String, b: String}
2392  E09{token: String}
2393  E10{}
2394  E11{arg: String, value: String, name: String, reason: String}
2395
2396type ExternalArgs is Data:
2397  ExternalArgs{name: String, args: List<&2, String>}
2398
2399type ScanTrace is Data:
2400  ScanTrace{path: List<&2, String>, events: List<&2, ScanEvent>,
2401    ended: Bool, external: Maybe<&2, ExternalArgs>,
2402    issue: Maybe<&2, ScanFault>}
2403
2404type ParseResult is Data:
2405  Accepted{path: List<&2, String>, fields: List<&2, Field>,
2406    external: Maybe<&2, ExternalArgs>}
2407  Rejected{path: List<&2, String>, fault: ScanFault}
2408  HelpRequested{path: List<&2, String>, long: Bool}
2409  VersionRequested{}
2410  SummaryRequired{}
2411
2412type Early is Data:
2413  EarlyHelp{long: Bool}
2414  EarlyVersion{}
2415
2416# An arg's hits so far: values holds a repeated option's newest first, so
2417# each hit conses, and resolve_argv_gather puts them back in argv order.
2418type Gather is Data:
2419  Gather{seen: Bool, values: List<&2, String>, count: Nat}
2420
2421type FieldBuild is Data:
2422  Built{fields: List<&2, Field>}
2423  BuildFault{fault: ScanFault}
2424
2425type EnvValue is Data:
2426  EnvValue{name: String, value: String}
2427
2428def host(width: Nat) -> Host:
2429  Host{width, [], False{}, False{}}
2430
2431def host_var(h: Host, name: String, value: String) -> Host:
2432  match h:
2433    case Host{width, vars, out_tty, err_tty}:
2434      Host{width, Var{name, value} <> vars, out_tty, err_tty}
2435
2436def var_value(vars: List<&2, Var>, +name: String) -> Maybe<&2, String>:
2437  match vars:
2438    case Nil{}: None{}
2439    case Var{key, value} <> tail:
2440      Bool.pick(Maybe<&2, String>, String.eq(key, name),
2441        Some{value}, var_value(tail, name))
2442
2443def host_value(h: Host, name: String) -> Maybe<&2, String>:
2444  match h:
2445    case Host{width, vars, out_tty, err_tty}:
2446      var_value(vars, name)
2447
2448type ArgInfo is Data:
2449  ArgInfo{id: String, usage: String, short: String, long: String,
2450    kind: UsageKind, attrs: List<&2, Item>}
2451
2452type OptFound is Data:
2453  OptFound{arg: ArgInfo, polarity: Bool}
2454
2455type SubFound is Data:
2456  SubFound{name: String, items: List<&2, Item>}
2457
2458# A subcommand a scope answers to, by name or alias.
2459type SubKey is Data:
2460  SubKey{name: String, aliases: List<&2, String>, children: List<&2, Item>}
2461
2462# An option name a scope answers to, with the match it makes.
2463type OptKey is Data:
2464  OptKey{name: String, found: OptFound}
2465
2466# An item resolve walks: an Arg with its usage parsed once, or another item.
2467type SpecItem is Data:
2468  SpecArg{info: ArgInfo}
2469  SpecOther{item: Item}
2470
2471# A scope with each usage parsed once: its option names (own, then global),
2472# the items resolve walks (global, then own), its positionals and
2473# subcommands, and the globals its subcommands inherit.
2474type Spec is Data:
2475  Spec{keys: List<&2, OptKey>, args: List<&2, SpecItem>,
2476    slots: List<&2, ArgInfo>, variadic: Bool, subs: List<&2, SubKey>,
2477    has_sub: Bool, external: Bool, needs_sub: Bool, version: Bool,
2478    digit_short: Bool, inherit: List<&2, SpecItem>}
2479
2480type Scope is Data:
2481  Scope{path: List<&2, String>, spec: Spec}
2482
2483type ScanState is Data:
2484  ScanState{scope: Scope, events: List<&2, ScanEvent>,
2485    resting: Bool, positional_count: Nat,
2486    external: Maybe<&2, ExternalArgs>, issue: Maybe<&2, ScanFault>}
2487
2488# Phases after which no token is an operand and `--` is plain.
2489type Tail is Data:
2490  Faulted{state: ScanState}
2491  Walk{state: ScanState}
2492  Stopped{state: ScanState}
2493
2494type Mode is Data:
2495  Open{state: ScanState}
2496  Fetch{state: ScanState, info: ArgInfo}
2497  Operands{state: ScanState, ended: Bool}
2498  Capture{state: ScanState, name: String}
2499  Closed{tail: Tail}
2500
2501def arg_info_parsed(parsed: Usage, usage: String,
2502    +attrs: List<&2, Item>) -> ArgInfo:
2503  match parsed:
2504    case Parsed{derived, short, long, kind}:
2505      ArgInfo{use_id(find_id(attrs), derived), usage, short, long,
2506        kind, attrs}
2507    case Invalid{col}:
2508      ArgInfo{"", usage, "", "", FlagKind{}, attrs}
2509
2510def arg_info(+usage: String, attrs: List<&2, Item>) -> ArgInfo:
2511  arg_info_parsed(parse_usage(usage), usage, attrs)
2512
2513def arg_display(info: ArgInfo) -> String:
2514  match info:
2515    case ArgInfo{id, +usage, +short, long, kind, attrs}:
2516      Bool.pick(String, Bool.and(Bool.not(String.is_empty(short)),
2517        Bool.not(String.is_empty(long))),
2518        String.drop(usage, Nat.add(String.length(short), 2n)), usage)
2519
2520def short_digit(short: String) -> Bool:
2521  match short:
2522    case SCon{Chr{45}, SCon{head, tail}}: Char.is_digit(head)
2523    case _: False{}
2524
2525def info_attrs(info: ArgInfo) -> List<&2, Item>:
2526  match info:
2527    case ArgInfo{id, usage, short, long, kind, attrs}: attrs
2528
2529def info_kind(info: ArgInfo) -> UsageKind:
2530  match info:
2531    case ArgInfo{id, usage, short, long, kind, attrs}: kind
2532
2533def info_id(info: ArgInfo) -> String:
2534  match info:
2535    case ArgInfo{id, usage, short, long, kind, attrs}: id
2536
2537def scope_path(scope: Scope) -> List<&2, String>:
2538  match scope:
2539    case Scope{path, spec}: path
2540
2541def arg_in_group(attrs: List<&2, Item>, name: String) -> Bool:
2542  List.contains(~String, ~String.eq,
2543    find_group_attrs(attrs), name)
2544
2545# Whether each arg items put in group name is global().
2546def group_global(items: List<&2, Item>, +name: String) -> Bool:
2547  match items:
2548    case Nil{}: True{}
2549    case Arg{usage, help, +attrs} <> tail:
2550      Bool.and(Bool.or(Bool.not(arg_in_group(attrs, name)), has_global(attrs)),
2551        group_global(tail, name))
2552    case head <> tail: group_global(tail, name)
2553
2554# What items pass down to their subcommands, in their order: each global()
2555# arg, and each exclusive or need_one rule over a group whose args in all are
2556# each global(), so that it holds in every subcommand below.
2557def global_go(items: List<&2, Item>, +all: List<&2, Item>) ->
2558    List<&2, Item>:
2559  match items:
2560    case Nil{}: Nil{}
2561    case Arg{usage, help, +attrs} <> tail:
2562      +rest = global_go(tail, all)
2563      Bool.pick(List<&2, Item>, has_global(attrs),
2564        Arg{usage, help, attrs} <> rest, rest)
2565    case Exclusive{+name} <> tail:
2566      +rest = global_go(tail, all)
2567      Bool.pick(List<&2, Item>, group_global(all, name),
2568        Exclusive{name} <> rest, rest)
2569    case NeedOne{+name} <> tail:
2570      +rest = global_go(tail, all)
2571      Bool.pick(List<&2, Item>, group_global(all, name),
2572        NeedOne{name} <> rest, rest)
2573    case head <> tail: global_go(tail, all)
2574
2575def global_items(+items: List<&2, Item>) -> List<&2, Item>:
2576  global_go(items, items)
2577
2578def kind_negatable(kind: UsageKind) -> Bool:
2579  match kind:
2580    case NegatableKind{}: True{}
2581    case _: False{}
2582
2583def negated_long(long: String) -> String:
2584  String.append("--no-", String.drop(long, 2n))
2585
2586def option_matches(+query: String, info: ArgInfo) ->
2587    Maybe<&2, OptFound>:
2588  match info:
2589    case ArgInfo{id, usage, +short, +long, +kind, +attrs}:
2590      +named = Bool.or(
2591        Bool.and(Bool.not(String.is_empty(short)), String.eq(query, short)),
2592        Bool.or(Bool.and(Bool.not(String.is_empty(long)),
2593          String.eq(query, long)),
2594          List.contains(~String, ~String.eq, find_aliases(attrs), query)))
2595      +negated = Bool.and(kind_negatable(kind),
2596        Bool.and(Bool.not(String.is_empty(long)),
2597          String.eq(query, negated_long(long))))
2598      Bool.pick(Maybe<&2, OptFound>, Bool.or(named, negated),
2599        Some{OptFound{ArgInfo{id, usage, short, long, kind, attrs},
2600          Bool.not(negated)}}, None{})
2601
2602def find_option(items: List<&2, Item>, +query: String) ->
2603    Maybe<&2, OptFound>:
2604  match items:
2605    case Nil{}: None{}
2606    case Arg{usage, help, attrs} <> tail:
2607      +found = option_matches(query, arg_info(usage, attrs))
2608      Bool.pick(Maybe<&2, OptFound>,
2609        Maybe.is_some(&2, OptFound, found), found,
2610        find_option(tail, query))
2611    case head <> tail: find_option(tail, query)
2612
2613def key_found(key: OptKey, query: String) -> Maybe<&2, OptFound>:
2614  match key:
2615    case OptKey{name, found}:
2616      Bool.pick(Maybe<&2, OptFound>, same_text(query, name),
2617        Some{found}, None{})
2618
2619# The first key named query; after a hit it only walks the rest.
2620def find_key(keys: List<&2, OptKey>, +query: String,
2621    found: Maybe<&2, OptFound>) -> Maybe<&2, OptFound>:
2622  match keys found:
2623    case Nil{} hit: hit
2624    case key <> tail Some{hit}: Some{hit}
2625    case key <> tail None{}: find_key(tail, query, key_found(key, query))
2626
2627# Whether keys' first key is named query. It only reads keys, so a walk
2628# that asks before it takes a key need not copy the keys it passes.
2629def head_named(keys: List<&2, OptKey>, +query: String) -> Bool:
2630  match keys:
2631    case Nil{}: False{}
2632    case OptKey{name, found} <> tail: same_text(query, name)
2633
2634# The position of the first of keys named query, where hit says whether
2635# keys' head is and at is its position. It only reads keys: a walk that
2636# takes them, as find_key does, copies every field of each key it passes.
2637def key_rank(keys: List<&2, OptKey>, +query: String, at: Nat, hit: Bool) ->
2638    Maybe<&2, Nat>:
2639  match keys hit:
2640    case Nil{} _: None{}
2641    case key <> tail True{}: Some{at}
2642    case key <> +tail False{}:
2643      next = head_named(tail, query)
2644      key_rank(tail, query, 1n+at, next)
2645
2646def key_found_at(key: Maybe<&2, OptKey>) -> Maybe<&2, OptFound>:
2647  match key:
2648    case None{}: None{}
2649    case Some{OptKey{name, found}}: Some{found}
2650
2651# List.get drops each key it passes whole, where a walk over OptKey would
2652# open it.
2653def key_at(rank: Maybe<&2, Nat>, keys: List<&2, OptKey>) ->
2654    Maybe<&2, OptFound>:
2655  match rank:
2656    case None{}: None{}
2657    case Some{at}: key_found_at(List.get(&2, OptKey, keys, at))
2658
2659# find_key(keys, query, None{}), copying only the key it finds.
2660def key_lookup(+keys: List<&2, OptKey>, +query: String) ->
2661    Maybe<&2, OptFound>:
2662  hit = head_named(keys, query)
2663  rank = key_rank(keys, query, 0n, hit)
2664  key_at(rank, keys)
2665
2666# Whether keys hold one named query, read without copying the key.
2667def key_known(+keys: List<&2, OptKey>, +query: String) -> Bool:
2668  hit = head_named(keys, query)
2669  Maybe.is_some(&2, Nat, key_rank(keys, query, 0n, hit))
2670
2671# A scope's own args answer before its globals, as in find_option.
2672def find_option_scope(scope: Scope, query: String) ->
2673    Maybe<&2, OptFound>:
2674  match scope:
2675    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2676        external, needs_sub, version, digit_short, inherit}}:
2677      key_lookup(keys, query)
2678
2679# Whether items alias query; after a hit it walks no further.
2680def has_alias(items: List<&2, Item>, +query: String, found: Bool) -> Bool:
2681  match items found:
2682    case _ True{}: True{}
2683    case Nil{} False{}: False{}
2684    case Alias{name} <> tail False{}:
2685      has_alias(tail, query, same_text(name, query))
2686    case head <> tail False{}: has_alias(tail, query, False{})
2687
2688def alias_hit(aliased: Bool, name: String, children: List<&2, Item>) ->
2689    Maybe<&2, SubFound>:
2690  match aliased:
2691    case True{}: Some{SubFound{name, children}}
2692    case False{}: None{}
2693
2694def sub_hit(same: Bool, name: String, +children: List<&2, Item>,
2695    query: String) -> Maybe<&2, SubFound>:
2696  match same:
2697    case True{}: Some{SubFound{name, children}}
2698    case False{}:
2699      alias_hit(has_alias(children, query, False{}), name, children)
2700
2701def sub_found(item: Item, +query: String) -> Maybe<&2, SubFound>:
2702  match item:
2703    case Sub{+name, about, children}:
2704      sub_hit(same_text(query, name), name, children, query)
2705    case _: None{}
2706
2707# The first Sub named or aliased query; after a hit it walks no further.
2708def find_sub_go(items: List<&2, Item>, +query: String,
2709    found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
2710  match items found:
2711    case _ Some{hit}: Some{hit}
2712    case Nil{} None{}: None{}
2713    case item <> tail None{}: find_sub_go(tail, query, sub_found(item, query))
2714
2715def find_sub(items: List<&2, Item>, +query: String) ->
2716    Maybe<&2, SubFound>:
2717  find_sub_go(items, query, None{})
2718
2719def positional_items(items: List<&2, Item>) -> List<&2, Item>:
2720  match items:
2721    case Nil{}: Nil{}
2722    case Arg{+usage, +help, +attrs} <> tail:
2723      +rest = positional_items(tail)
2724      Bool.pick(List<&2, Item>,
2725        usage_kind(~kind_pos, parse_usage(usage)),
2726        Arg{usage, help, attrs} <> rest, rest)
2727    case head <> tail: positional_items(tail)
2728
2729def has_variadic_pos(items: List<&2, Item>) -> Bool:
2730  match items:
2731    case Nil{}: False{}
2732    case Arg{usage, help, attrs} <> tail:
2733      Bool.or(usage_kind(~kind_variadic_pos, parse_usage(usage)),
2734        has_variadic_pos(tail))
2735    case head <> tail: has_variadic_pos(tail)
2736
2737def positional_can_take(items: List<&2, Item>, count: Nat) -> Bool:
2738  +slots = positional_items(items)
2739  Bool.or(Nat.is_lt(count, List.length(&2, Item, slots)),
2740    has_variadic_pos(slots))
2741
2742# name as a key of info; only info's --no- long clears the flag, as in
2743# option_matches.
2744def opt_key(+name: String, info: ArgInfo) -> OptKey:
2745  match info:
2746    case ArgInfo{id, usage, short, +long, +kind, attrs}:
2747      OptKey{name, OptFound{ArgInfo{id, usage, short, long, kind, attrs},
2748        Bool.not(Bool.and(kind_negatable(kind),
2749          Bool.and(Bool.not(String.is_empty(long)),
2750            String.eq(name, negated_long(long)))))}}
2751
2752def name_keys(names: List<&2, String>, +info: ArgInfo) ->
2753    List<&2, OptKey>:
2754  match names:
2755    case Nil{}: Nil{}
2756    case name <> tail: opt_key(name, info) <> name_keys(tail, info)
2757
2758def nonempty_name(+name: String) -> List<&2, String>:
2759  Bool.pick(List<&2, String>, String.is_empty(name), [], [name])
2760
2761# The names option_matches accepts for info, in its order.
2762def info_names(info: ArgInfo) -> List<&2, String>:
2763  match info:
2764    case ArgInfo{id, usage, short, +long, kind, attrs}:
2765      List.append(&2, String, nonempty_name(short),
2766        List.append(&2, String, nonempty_name(long),
2767          List.append(&2, String, find_aliases(attrs),
2768            Bool.pick(List<&2, String>, Bool.and(kind_negatable(kind),
2769              Bool.not(String.is_empty(long))),
2770              [negated_long(long)], []))))
2771
2772def spec_keys(items: List<&2, SpecItem>) -> List<&2, OptKey>:
2773  match items:
2774    case Nil{}: Nil{}
2775    case SpecArg{+info} <> tail:
2776      List.append(&2, OptKey, name_keys(info_names(info), info),
2777        spec_keys(tail))
2778    case head <> tail: spec_keys(tail)
2779
2780# items less their Subs, each Arg's usage parsed once.
2781def spec_items(items: List<&2, Item>) -> List<&2, SpecItem>:
2782  match items:
2783    case Nil{}: Nil{}
2784    case Arg{usage, help, attrs} <> tail:
2785      SpecArg{arg_info(usage, attrs)} <> spec_items(tail)
2786    case Sub{name, about, children} <> tail: spec_items(tail)
2787    case head <> tail: SpecOther{head} <> spec_items(tail)
2788
2789# global_go over items parsed, with all as written.
2790def global_spec(items: List<&2, SpecItem>, +all: List<&2, Item>) ->
2791    List<&2, SpecItem>:
2792  match items:
2793    case Nil{}: Nil{}
2794    case SpecArg{+info} <> tail:
2795      +rest = global_spec(tail, all)
2796      attrs = info_attrs(info)
2797      Bool.pick(List<&2, SpecItem>, has_global(attrs),
2798        SpecArg{info} <> rest, rest)
2799    case SpecOther{Exclusive{+name}} <> tail:
2800      +rest = global_spec(tail, all)
2801      Bool.pick(List<&2, SpecItem>, group_global(all, name),
2802        SpecOther{Exclusive{name}} <> rest, rest)
2803    case SpecOther{NeedOne{+name}} <> tail:
2804      +rest = global_spec(tail, all)
2805      Bool.pick(List<&2, SpecItem>, group_global(all, name),
2806        SpecOther{NeedOne{name}} <> rest, rest)
2807    case head <> tail: global_spec(tail, all)
2808
2809def spec_slots(items: List<&2, SpecItem>) -> List<&2, ArgInfo>:
2810  match items:
2811    case Nil{}: Nil{}
2812    case SpecArg{+info} <> tail:
2813      +rest = spec_slots(tail)
2814      Bool.pick(List<&2, ArgInfo>, kind_pos(info_kind(info)),
2815        info <> rest, rest)
2816    case head <> tail: spec_slots(tail)
2817
2818def slots_variadic(slots: List<&2, ArgInfo>) -> Bool:
2819  match slots:
2820    case Nil{}: False{}
2821    case info <> tail:
2822      Bool.or(kind_variadic_pos(info_kind(info)), slots_variadic(tail))
2823
2824def info_digit_short(info: ArgInfo) -> Bool:
2825  match info:
2826    case ArgInfo{id, usage, short, long, kind, attrs}: short_digit(short)
2827
2828def spec_digit_short(items: List<&2, SpecItem>) -> Bool:
2829  match items:
2830    case Nil{}: False{}
2831    case SpecArg{info} <> tail:
2832      Bool.or(info_digit_short(info), spec_digit_short(tail))
2833    case head <> tail: spec_digit_short(tail)
2834
2835def raw_subs(items: List<&2, Item>) -> List<&2, SubKey>:
2836  match items:
2837    case Nil{}: Nil{}
2838    case Sub{name, about, +children} <> tail:
2839      SubKey{name, find_aliases(children), children} <> raw_subs(tail)
2840    case head <> tail: raw_subs(tail)
2841
2842# items as a scope inheriting globals; its subcommands stay unparsed until
2843# the scan enters one.
2844def spec_node(+items: List<&2, Item>, +globals: List<&2, SpecItem>) ->
2845    Spec:
2846  +own = spec_items(items)
2847  +args = List.append(&2, SpecItem, globals, own)
2848  +slots = spec_slots(own)
2849  Spec{List.append(&2, OptKey, spec_keys(own), spec_keys(globals)),
2850    args, slots, slots_variadic(slots), raw_subs(items),
2851    has_sub_item(items), has_external(items), needs_sub(items),
2852    has_version_item(items), spec_digit_short(args),
2853    List.append(&2, SpecItem, global_spec(own, items), globals)}
2854
2855def scan_start(c: Cmd) -> ScanState:
2856  match c:
2857    case Cmd{name, about, items}:
2858      ScanState{Scope{[], spec_node(items, [])}, [], False{}, 0n, None{},
2859        None{}}
2860
2861def scan_event(state: ScanState, event: ScanEvent) -> ScanState:
2862  match state:
2863    case ScanState{scope, events, resting, positional_count,
2864        external, issue}:
2865      ScanState{scope, event <> events, resting,
2866        positional_count, external, issue}
2867
2868def first_issue(issue: Maybe<&2, ScanFault>, fault: ScanFault) ->
2869    Maybe<&2, ScanFault>:
2870  match issue:
2871    case None{}: Some{fault}
2872    case Some{old}: Some{old}
2873
2874def scan_error(state: ScanState, fault: ScanFault) -> ScanState:
2875  match state:
2876    case ScanState{scope, events, resting, positional_count,
2877        external, issue}:
2878      ScanState{scope, events, resting, positional_count,
2879        external, first_issue(issue, fault)}
2880
2881def has_hyphen(items: List<&2, Item>) -> Bool:
2882  match items:
2883    case Nil{}: False{}
2884    case Hyphen{} <> tail: True{}
2885    case head <> tail: has_hyphen(tail)
2886
2887def has_as_f32(items: List<&2, Item>) -> Bool:
2888  match items:
2889    case Nil{}: False{}
2890    case As{TF32{}} <> tail: True{}
2891    case head <> tail: has_as_f32(tail)
2892
2893# What the slot that takes the next operand allows: rest(), values that
2894# start with "-", and as_f32().
2895type SlotTraits is Data:
2896  SlotTraits{rest: Bool, hyphen: Bool, f32: Bool}
2897
2898# Whether slots' first slot takes the operand after count others: it is
2899# the count'th, or variadic.
2900def slot_here(slots: List<&2, ArgInfo>, count: Nat) -> Bool:
2901  match slots:
2902    case Nil{}: False{}
2903    case ArgInfo{id, usage, short, long, kind, attrs} <> tail:
2904      Bool.or(Nat.is_eq(count, 0n), kind_variadic_pos(kind))
2905
2906# The traits of the first of slots that takes the operand after count
2907# others, where here says whether slots' head does. It only reads slots:
2908# a walk that returned the slot would copy each of its fields.
2909def slot_traits_go(slots: List<&2, ArgInfo>, count: Nat, here: Bool) ->
2910    SlotTraits:
2911  match slots here:
2912    case Nil{} _: SlotTraits{False{}, False{}, False{}}
2913    case ArgInfo{id, usage, short, long, kind, +attrs} <> tail True{}:
2914      SlotTraits{has_rest(attrs), has_hyphen(attrs), has_as_f32(attrs)}
2915    case info <> +tail False{}:
2916      +rest = Nat.sub(count, 1n)
2917      next = slot_here(tail, rest)
2918      slot_traits_go(tail, rest, next)
2919
2920def scope_slot_traits(scope: Scope, +count: Nat) -> SlotTraits:
2921  match scope:
2922    case Scope{path, Spec{keys, args, +slots, variadic, subs, has_sub,
2923        external, needs_sub, version, digit_short, inherit}}:
2924      slot_traits_go(slots, count, slot_here(slots, count))
2925
2926def traits_rest(traits: SlotTraits) -> Bool:
2927  match traits:
2928    case SlotTraits{rest, hyphen, f32}: rest
2929
2930def is_negative_number(+s: String) -> Bool:
2931  Bool.and(String.starts_with(s, "-"), valid_f32(s))
2932
2933def before_equal(s: String) -> String:
2934  match s:
2935    case SNil{}: SNil{}
2936    case SCon{Chr{61}, tail}: SNil{}
2937    case SCon{head, tail}: SCon{head, before_equal(tail)}
2938
2939def after_equal(s: String) -> Maybe<&2, String>:
2940  match s:
2941    case SNil{}: None{}
2942    case SCon{Chr{61}, tail}: Some{tail}
2943    case SCon{head, tail}: after_equal(tail)
2944
2945def first_option_name(+token: String) -> String:
2946  Bool.pick(String, String.starts_with(token, "--"),
2947    before_equal(token), String.take(token, 2n))
2948
2949def scope_option_known(scope: Scope, token: String) -> Bool:
2950  match scope:
2951    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2952        external, needs_sub, version, digit_short, inherit}}:
2953      key_known(keys, first_option_name(token))
2954
2955def scope_builtin_known(scope: Scope, +token: String) -> Bool:
2956  match scope:
2957    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2958        external, needs_sub, version, digit_short, inherit}}:
2959      +name = first_option_name(token)
2960      Bool.or(String.eq(name, "-h"),
2961        Bool.or(String.eq(name, "--help"),
2962          Bool.and(List.is_empty(&2, String, path),
2963            Bool.and(version,
2964              Bool.or(String.eq(name, "-V"),
2965                String.eq(name, "--version"))))))
2966
2967def scope_any_option_known(+scope: Scope, +token: String) -> Bool:
2968  Bool.or(scope_option_known(scope, token),
2969    scope_builtin_known(scope, token))
2970
2971def scan_operand_ok(state: ScanState, token: String) -> ScanState:
2972  match state:
2973    case ScanState{+scope, events, resting, +positional_count,
2974        external, issue}:
2975      ScanState{scope, Operand{token} <> events,
2976        Bool.or(resting,
2977          traits_rest(scope_slot_traits(scope, positional_count))),
2978        Nat.add(positional_count, 1n), external, issue}
2979
2980def scan_operand_case(allowed: Bool, state: ScanState,
2981    token: String) -> ScanState:
2982  match allowed:
2983    case True{}: scan_operand_ok(state, token)
2984    case False{}: scan_error(state, E09{token})
2985
2986def scope_can_take(scope: Scope, count: Nat) -> Bool:
2987  match scope:
2988    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2989        external, needs_sub, version, digit_short, inherit}}:
2990      Bool.or(Nat.is_lt(count, List.length(&2, ArgInfo, slots)), variadic)
2991
2992def scan_operand(state: ScanState, token: String) -> ScanState:
2993  match state:
2994    case ScanState{+scope, events, resting, +positional_count,
2995        external, issue}:
2996      scan_operand_case(scope_can_take(scope, positional_count),
2997        ScanState{scope, events, resting, positional_count,
2998          external, issue}, token)
2999
3000def spec_inherit(spec: Spec) -> List<&2, SpecItem>:
3001  match spec:
3002    case Spec{keys, args, slots, variadic, subs, has_sub, external,
3003        needs_sub, version, digit_short, inherit}: inherit
3004
3005def event_id(event: ScanEvent) -> Maybe<&2, String>:
3006  match event:
3007    case FlagHit{id, value}: Some{id}
3008    case CountHit{id}: Some{id}
3009    case ValueHit{id, value}: Some{id}
3010    case _: None{}
3011
3012def spec_info_found(item: SpecItem, id: String) -> Maybe<&2, ArgInfo>:
3013  match item:
3014    case SpecArg{+info}:
3015      own = info_id(info)
3016      Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
3017    case SpecOther{other}: None{}
3018
3019# find_arg_info over parsed items; after a hit it only walks the rest.
3020def find_spec_info(items: List<&2, SpecItem>, +id: String,
3021    found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
3022  match items found:
3023    case Nil{} hit: hit
3024    case item <> tail Some{hit}: Some{hit}
3025    case item <> tail None{}:
3026      find_spec_info(tail, id, spec_info_found(item, id))
3027
3028# An arg's hits from before the scan enters a subcommand stay only when the
3029# scope being left passes the arg down as a global (SPEC 8.4); operands,
3030# entries, help and version stay.
3031def event_inherited(found: Maybe<&2, String>,
3032    +globals: List<&2, SpecItem>) -> Bool:
3033  match found:
3034    case None{}: True{}
3035    case Some{id}:
3036      Maybe.is_some(&2, ArgInfo, find_spec_info(globals, id, None{}))
3037
3038def events_kept(keep: Bool, event: ScanEvent,
3039    rest: List<&2, ScanEvent>) -> List<&2, ScanEvent>:
3040  match keep:
3041    case True{}: event <> rest
3042    case False{}: rest
3043
3044def events_inherited(events: List<&2, ScanEvent>,
3045    +globals: List<&2, SpecItem>) -> List<&2, ScanEvent>:
3046  match events:
3047    case Nil{}: Nil{}
3048    case +event <> tail:
3049      events_kept(event_inherited(event_id(event), globals), event,
3050        events_inherited(tail, globals))
3051
3052def scan_descend(state: ScanState, sub: SubKey) -> ScanState:
3053  match state:
3054    case ScanState{scope, events, resting, positional_count,
3055        external, issue}:
3056      match scope:
3057        case Scope{path, spec}:
3058          match sub:
3059            case SubKey{+name, aliases, children}:
3060              +globals = spec_inherit(spec)
3061              ScanState{Scope{
3062                List.append(&2, String, path, [name]),
3063                spec_node(children, globals)},
3064                Enter{name} <> events_inherited(events, globals), resting, 0n,
3065                external, issue}
3066
3067def scan_external(state: ScanState, name: String,
3068    args: List<&2, String>) -> ScanState:
3069  match state:
3070    case ScanState{scope, events, resting, positional_count,
3071        external, issue}:
3072      ScanState{scope, events, resting, positional_count,
3073        Some{ExternalArgs{name, args}}, issue}
3074
3075# E04 quotes the first element that fails.
3076def scan_value_result(found: Maybe<&2, Bad>, state: ScanState,
3077    info: ArgInfo, value: String) -> ScanState:
3078  match found:
3079    case None{}:
3080      match info:
3081        case ArgInfo{id, usage, short, long, kind, attrs}:
3082          scan_event(state, ValueHit{id, value})
3083    case Some{Bad{bad, why}}:
3084      scan_error(state, E04{arg_display(info), bad, why})
3085
3086def scan_value(state: ScanState, +info: ArgInfo,
3087    +value: String) -> ScanState:
3088  scan_value_result(value_bad(info_attrs(info), value), state, info, value)
3089
3090def scan_simple_hit(state: ScanState, info: ArgInfo,
3091    polarity: Bool) -> ScanState:
3092  match info:
3093    case ArgInfo{id, usage, short, long, kind, attrs}:
3094      match kind:
3095        case CountKind{}: scan_event(state, CountHit{id})
3096        case _: scan_event(state, FlagHit{id, polarity})
3097
3098def scan_settle_case(resting: Bool, issue: Maybe<&2, ScanFault>,
3099    scope: Scope, events: List<&2, ScanEvent>, positional_count: Nat,
3100    external: Maybe<&2, ExternalArgs>) -> Mode:
3101  match resting issue:
3102    case True{} None{}:
3103      Operands{ScanState{scope, events, True{}, positional_count, external,
3104        None{}}, False{}}
3105    case True{} Some{fault}:
3106      Operands{ScanState{scope, events, True{}, positional_count, external,
3107        Some{fault}}, False{}}
3108    case False{} None{}:
3109      Open{ScanState{scope, events, False{}, positional_count, external,
3110        None{}}}
3111    case False{} Some{fault}:
3112      Closed{Faulted{ScanState{scope, events, False{}, positional_count,
3113        external, Some{fault}}}}
3114
3115def scan_settle(state: ScanState) -> Mode:
3116  match state:
3117    case ScanState{scope, events, resting, positional_count, external,
3118        issue}:
3119      scan_settle_case(resting, issue, scope, events, positional_count,
3120        external)
3121
3122def help_flag_case(long: Bool, short: Bool) -> Maybe<&2, Bool>:
3123  match long short:
3124    case True{} _: Some{True{}}
3125    case False{} True{}: Some{False{}}
3126    case False{} False{}: None{}
3127
3128# Which help flag a token is: Some{True{}} for --help, Some{False{}} for -h.
3129def help_flag(+token: String) -> Maybe<&2, Bool>:
3130  help_flag_case(String.eq(token, "--help"), String.eq(token, "-h"))
3131
3132def scan_faulted_case(help: Maybe<&2, Bool>, state: ScanState) -> Tail:
3133  match help:
3134    case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3135    case None{}: Faulted{state}
3136
3137def scan_faulted(state: ScanState, token: String) -> Tail:
3138  scan_faulted_case(help_flag(token), state)
3139
3140def scope_digit_short(scope: Scope) -> Bool:
3141  match scope:
3142    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3143        external, needs_sub, version, digit_short, inherit}}: digit_short
3144
3145def arg_has_hyphen(info: ArgInfo) -> Bool:
3146  match info:
3147    case ArgInfo{id, usage, short, long, kind, attrs}:
3148      has_hyphen(attrs)
3149
3150def arg_is_f32(info: ArgInfo) -> Bool:
3151  match info:
3152    case ArgInfo{id, usage, short, long, kind, attrs}:
3153      has_as_f32(attrs)
3154
3155# Whether info's value may be token, which starts with "-".
3156def fetch_dash_allowed(scope: Scope, +info: ArgInfo,
3157    +token: String) -> Bool:
3158  Bool.and(Bool.not(String.eq(token, "--")),
3159    Bool.or(String.eq(token, "-"),
3160      Bool.or(arg_has_hyphen(info),
3161        Bool.and(arg_is_f32(info),
3162          Bool.and(is_negative_number(token),
3163            Bool.not(scope_digit_short(scope)))))))
3164
3165def fetch_allowed_case(dash: Bool, scope: Scope, info: ArgInfo,
3166    token: String) -> Bool:
3167  match dash:
3168    case True{}: fetch_dash_allowed(scope, info, token)
3169    case False{}: True{}
3170
3171def fetch_allowed(scope: Scope, info: ArgInfo, +token: String) -> Bool:
3172  fetch_allowed_case(String.starts_with(token, "-"), scope, info, token)
3173
3174def scan_fetch_result(allowed: Bool, state: ScanState,
3175    info: ArgInfo, token: String) -> Mode:
3176  match allowed:
3177    case True{}: scan_settle(scan_value(state, info, token))
3178    case False{}:
3179      Closed{scan_faulted(scan_error(state, E03{arg_display(info)}), token)}
3180
3181def scan_state_scope(state: ScanState) -> Scope:
3182  match state:
3183    case ScanState{scope, events, resting, positional_count,
3184        external, issue}: scope
3185
3186def scan_fetch(+state: ScanState, +info: ArgInfo,
3187    +token: String) -> Mode:
3188  scan_fetch_result(fetch_allowed(scan_state_scope(state), info, token),
3189    state, info, token)
3190
3191def bare_value(attrs: List<&2, Item>) -> String:
3192  match attrs:
3193    case Nil{}: ""
3194    case Bare{value} <> tail: value
3195    case head <> tail: bare_value(tail)
3196
3197def scan_option(attached: Maybe<&2, String>, state: ScanState,
3198    info: ArgInfo) -> Mode:
3199  match attached:
3200    case Some{value}: scan_settle(scan_value(state, info, value))
3201    case None{}: Fetch{state, info}
3202
3203def info_bare_value(info: ArgInfo) -> String:
3204  match info:
3205    case ArgInfo{id, usage, short, long, kind, attrs}:
3206      bare_value(attrs)
3207
3208def scan_bare_option(attached: Maybe<&2, String>, state: ScanState,
3209    +info: ArgInfo) -> Mode:
3210  match attached:
3211    case Some{value}: scan_settle(scan_value(state, info, value))
3212    case None{}:
3213      scan_settle(scan_value(state, info, info_bare_value(info)))
3214
3215def scan_simple_option(attached: Maybe<&2, String>, state: ScanState,
3216    info: ArgInfo, polarity: Bool) -> Mode:
3217  match attached:
3218    case Some{value}:
3219      scan_settle(scan_error(state, E05{arg_display(info), value}))
3220    case None{}: scan_settle(scan_simple_hit(state, info, polarity))
3221
3222def scan_found_kind(kind: UsageKind, attached: Maybe<&2, String>,
3223    state: ScanState, info: ArgInfo, polarity: Bool) -> Mode:
3224  match kind:
3225    case FlagKind{}:
3226      scan_simple_option(attached, state, info, polarity)
3227    case CountKind{}:
3228      scan_simple_option(attached, state, info, polarity)
3229    case NegatableKind{}:
3230      scan_simple_option(attached, state, info, polarity)
3231    case OptionKind{many}:
3232      scan_option(attached, state, info)
3233    case BareOptionKind{}:
3234      scan_bare_option(attached, state, info)
3235    case PosKind{required, many}:
3236      scan_settle(state)
3237
3238def scan_found(found: OptFound, attached: Maybe<&2, String>,
3239    state: ScanState) -> Mode:
3240  match found:
3241    case OptFound{+arg, polarity}:
3242      scan_found_kind(info_kind(arg), attached, state, arg, polarity)
3243
3244def scope_version(scope: Scope) -> Bool:
3245  match scope:
3246    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3247        external, needs_sub, version, digit_short, inherit}}:
3248      Bool.and(List.is_empty(&2, String, path), version)
3249
3250def scan_builtin_hit(attached: Maybe<&2, String>, state: ScanState,
3251    event: ScanEvent, name: String) -> Mode:
3252  match attached:
3253    case Some{value}: scan_settle(scan_error(state, E05{name, value}))
3254    case None{}: Closed{Stopped{scan_event(state, event)}}
3255
3256def traits_hyphen(traits: SlotTraits) -> Bool:
3257  match traits:
3258    case SlotTraits{rest, hyphen, f32}: Bool.or(rest, hyphen)
3259
3260# Whether the slot after state's operands takes hyphen values, as a rest()
3261# slot does until it has its first value.
3262def scan_state_hyphen(state: ScanState) -> Bool:
3263  match state:
3264    case ScanState{scope, events, resting, positional_count,
3265        external, issue}:
3266      traits_hyphen(scope_slot_traits(scope, positional_count))
3267
3268# The long token that before_equal and after_equal split into name and
3269# attached.
3270def long_token(attached: Maybe<&2, String>, name: String) -> String:
3271  match attached:
3272    case Some{value}: String.append(name, SCon{Chr{61}, value})
3273    case None{}: name
3274
3275def scan_unknown_long_case(hyphen: Bool, name: String,
3276    attached: Maybe<&2, String>, state: ScanState) -> Mode:
3277  match hyphen:
3278    case True{}: scan_settle(scan_operand(state, long_token(attached, name)))
3279    case False{}: scan_settle(scan_error(state, E01{name}))
3280
3281# A long token that names no option, declared or built-in: a value of the
3282# next slot if that slot takes hyphen values, else unknown. Only here is the
3283# token rebuilt from its name and attached value, so a token that names an
3284# option is split once and not kept.
3285def scan_unknown_long(name: String, attached: Maybe<&2, String>,
3286    +state: ScanState) -> Mode:
3287  scan_unknown_long_case(scan_state_hyphen(state), name, attached, state)
3288
3289def scan_builtin_other(version: Bool, name: String,
3290    attached: Maybe<&2, String>, state: ScanState) -> Mode:
3291  match version:
3292    case True{}:
3293      scan_builtin_hit(attached, state, VersionHit{}, "--version")
3294    case False{}: scan_unknown_long(name, attached, state)
3295
3296def scan_builtin_long_case(help: Bool, +name: String,
3297    attached: Maybe<&2, String>, +state: ScanState) -> Mode:
3298  match help:
3299    case True{}:
3300      scan_builtin_hit(attached, state, HelpHit{True{}}, "--help")
3301    case False{}:
3302      scan_builtin_other(Bool.and(String.eq(name, "--version"),
3303        scope_version(scan_state_scope(state))), name, attached, state)
3304
3305def scan_builtin_long(+name: String, attached: Maybe<&2, String>,
3306    state: ScanState) -> Mode:
3307  scan_builtin_long_case(String.eq(name, "--help"), name, attached, state)
3308
3309def scan_long_found(found: Maybe<&2, OptFound>, name: String,
3310    attached: Maybe<&2, String>, state: ScanState) -> Mode:
3311  match found:
3312    case Some{option}: scan_found(option, attached, state)
3313    case None{}: scan_builtin_long(name, attached, state)
3314
3315def scan_long(+token: String, +state: ScanState) -> Mode:
3316  +name = before_equal(token)
3317  scan_long_found(find_option_scope(scan_state_scope(state), name),
3318    name, after_equal(token), state)
3319
3320# The keys of state's scope.
3321def scan_state_keys(state: ScanState) -> List<&2, OptKey>:
3322  match state:
3323    case ScanState{Scope{path, Spec{keys, args, slots, variadic, subs,
3324        has_sub, external, needs_sub, version, digit_short, inherit}},
3325        events, resting, positional_count, ext, issue}: keys
3326
3327def find_short_for_head(chars: String, keys: List<&2, OptKey>) ->
3328    Maybe<&2, OptFound>:
3329  match chars:
3330    case SNil{}: None{}
3331    case SCon{head, tail}:
3332      key_lookup(keys, String.append("-", Char.show(head)))
3333
3334def short_attached(chars: String) -> String:
3335  match chars:
3336    case SCon{Chr{61}, tail}: tail
3337    case _: chars
3338
3339def scan_short_value_attached(value: String, state: ScanState,
3340    info: ArgInfo) -> Mode:
3341  match value:
3342    case SNil{}: Fetch{state, info}
3343    case _: scan_settle(scan_value(state, info, value))
3344
3345def scan_short_value(chars: String, state: ScanState,
3346    info: ArgInfo) -> Mode:
3347  scan_short_value_attached(short_attached(chars), state, info)
3348
3349def scan_short_bare_value(chars: String, state: ScanState,
3350    +info: ArgInfo) -> Mode:
3351  match chars:
3352    case SNil{}:
3353      scan_settle(scan_value(state, info, info_bare_value(info)))
3354    case _:
3355      scan_settle(scan_value(state, info, short_attached(chars)))
3356
3357def scan_short_version(enabled: Bool, state: ScanState) -> Mode:
3358  match enabled:
3359    case True{}: Closed{Stopped{scan_event(state, VersionHit{})}}
3360    case False{}: scan_settle(scan_error(state, E01{"-V"}))
3361
3362def scan_short_go(chars: String, +state: ScanState,
3363    found: Maybe<&2, OptFound>) -> Mode:
3364  match chars found:
3365    case SNil{} _:
3366      scan_settle(state)
3367    case SCon{Chr{104}, +tail} None{}:
3368      Closed{Stopped{scan_event(state, HelpHit{False{}})}}
3369    case SCon{Chr{86}, tail} None{}:
3370      scan_short_version(scope_version(scan_state_scope(state)), state)
3371    case SCon{head, tail} None{}:
3372      scan_settle(scan_error(state,
3373        E01{String.append("-", Char.show(head))}))
3374    case SCon{head, tail} Some{OptFound{arg, polarity}}:
3375      match arg:
3376        case ArgInfo{id, usage, short, long, kind, attrs}:
3377          match kind:
3378            case FlagKind{}:
3379              +next = scan_event(state, FlagHit{id, polarity})
3380              scan_short_go(tail, next,
3381                find_short_for_head(tail, scan_state_keys(next)))
3382            case CountKind{}:
3383              +next = scan_event(state, CountHit{id})
3384              scan_short_go(tail, next,
3385                find_short_for_head(tail, scan_state_keys(next)))
3386            case NegatableKind{}:
3387              +next = scan_event(state, FlagHit{id, polarity})
3388              scan_short_go(tail, next,
3389                find_short_for_head(tail, scan_state_keys(next)))
3390            case OptionKind{many}:
3391              scan_short_value(tail, state,
3392                ArgInfo{id, usage, short, long, OptionKind{many}, attrs})
3393            case BareOptionKind{}:
3394              scan_short_bare_value(tail, state,
3395                ArgInfo{id, usage, short, long, BareOptionKind{}, attrs})
3396            case PosKind{required, many}:
3397              scan_settle(state)
3398
3399# The lookup takes only the scope's keys: an accessor for them would take
3400# the state and copy each of its fields.
3401def scan_short(+state: ScanState, token: String) -> Mode:
3402  match state:
3403    case ScanState{Scope{path, Spec{keys, args, slots, variadic, subs,
3404        has_sub, external, needs_sub, version, digit_short, inherit}},
3405        events, resting, positional_count, ext, issue}:
3406      +chars = String.drop(token, 1n)
3407      found = find_short_for_head(chars, keys)
3408      scan_short_go(chars, state, found)
3409
3410def scope_external(scope: Scope) -> Bool:
3411  match scope:
3412    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3413        external, needs_sub, version, digit_short, inherit}}: external
3414
3415def scope_has_sub(scope: Scope) -> Bool:
3416  match scope:
3417    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3418        external, needs_sub, version, digit_short, inherit}}: has_sub
3419
3420def sub_key_aliased(aliased: Bool, name: String, aliases: List<&2, String>,
3421    children: List<&2, Item>) -> Maybe<&2, SubKey>:
3422  match aliased:
3423    case True{}: Some{SubKey{name, aliases, children}}
3424    case False{}: None{}
3425
3426def sub_key_hit(same: Bool, name: String, +aliases: List<&2, String>,
3427    children: List<&2, Item>, query: String) -> Maybe<&2, SubKey>:
3428  match same:
3429    case True{}: Some{SubKey{name, aliases, children}}
3430    case False{}:
3431      sub_key_aliased(List.contains(~String, ~String.eq, aliases, query),
3432        name, aliases, children)
3433
3434def sub_key_found(key: SubKey, +query: String) -> Maybe<&2, SubKey>:
3435  match key:
3436    case SubKey{+name, aliases, children}:
3437      sub_key_hit(same_text(query, name), name, aliases, children, query)
3438
3439# find_sub over a scope's subcommands; after a hit it only walks the rest.
3440def find_sub_key(subs: List<&2, SubKey>, +query: String,
3441    found: Maybe<&2, SubKey>) -> Maybe<&2, SubKey>:
3442  match subs found:
3443    case Nil{} hit: hit
3444    case key <> tail Some{hit}: Some{hit}
3445    case key <> tail None{}:
3446      find_sub_key(tail, query, sub_key_found(key, query))
3447
3448def scope_find_sub(scope: Scope, query: String) -> Maybe<&2, SubKey>:
3449  match scope:
3450    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3451        external, needs_sub, version, digit_short, inherit}}:
3452      find_sub_key(subs, query, None{})
3453
3454def scan_sub_unknown(allow_external: Bool, state: ScanState,
3455    token: String) -> Mode:
3456  match allow_external:
3457    case True{}: Capture{state, token}
3458    case False{}: Closed{Stopped{scan_error(state, E02{token})}}
3459
3460def scan_sub_found(found: Maybe<&2, SubKey>, +state: ScanState,
3461    token: String) -> Mode:
3462  match found:
3463    case Some{sub}: scan_settle(scan_descend(state, sub))
3464    case None{}:
3465      scan_sub_unknown(scope_external(scan_state_scope(state)),
3466        state, token)
3467
3468def unknown_fault(dash: Bool, token: String) -> ScanFault:
3469  match dash:
3470    case True{}: E01{token}
3471    case False{}: E02{token}
3472
3473def help_missing_case(help: Maybe<&2, Bool>, +token: String,
3474    state: ScanState) -> Tail:
3475  match help:
3476    case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3477    case None{}:
3478      Stopped{scan_error(state,
3479        unknown_fault(String.starts_with(token, "-"), token))}
3480
3481def help_missing(+token: String, state: ScanState) -> Tail:
3482  help_missing_case(help_flag(token), token, state)
3483
3484def help_walk(found: Maybe<&2, SubKey>, state: ScanState,
3485    token: String) -> Tail:
3486  match found:
3487    case Some{sub}: Walk{scan_descend(state, sub)}
3488    case None{}: help_missing(token, state)
3489
3490def scan_non_option_sub_case(help: Bool, +token: String,
3491    +state: ScanState) -> Mode:
3492  match help:
3493    case True{}: Closed{Walk{state}}
3494    case False{}:
3495      scan_sub_found(scope_find_sub(scan_state_scope(state), token),
3496        state, token)
3497
3498def scan_non_option_sub(+token: String, state: ScanState) -> Mode:
3499  scan_non_option_sub_case(String.eq(token, "help"), token, state)
3500
3501def scan_non_option_case(has_sub: Bool, token: String,
3502    state: ScanState) -> Mode:
3503  match has_sub:
3504    case True{}: scan_non_option_sub(token, state)
3505    case False{}: scan_settle(scan_operand(state, token))
3506
3507def scan_non_option(token: String, +state: ScanState) -> Mode:
3508  scan_non_option_case(scope_has_sub(scan_state_scope(state)), token, state)
3509
3510# Whether a slot that takes hyphen values takes a dash token: no option in
3511# scope is named by it. The lookup runs only for such a slot, which a rest()
3512# slot is until it has its first value.
3513def hyphen_operand(hyphen: Bool, scope: Scope, token: String) -> Bool:
3514  match hyphen:
3515    case True{}: Bool.not(scope_any_option_known(scope, token))
3516    case False{}: False{}
3517
3518# Whether an f32 slot takes token as a negative number.
3519def f32_operand(f32: Bool, scope: Scope, token: String) -> Bool:
3520  match f32:
3521    case True{}:
3522      Bool.and(is_negative_number(token), Bool.not(scope_digit_short(scope)))
3523    case False{}: False{}
3524
3525def slot_candidate(traits: SlotTraits, +scope: Scope, +token: String) ->
3526    Bool:
3527  match traits:
3528    case SlotTraits{rest, hyphen, f32}:
3529      Bool.or(hyphen_operand(Bool.or(rest, hyphen), scope, token),
3530        f32_operand(f32, scope, token))
3531
3532def scan_hyphen_candidate(state: ScanState, +token: String) -> Bool:
3533  match state:
3534    case ScanState{+scope, events, resting, positional_count,
3535        external, issue}:
3536      slot_candidate(scope_slot_traits(scope, positional_count), scope, token)
3537
3538def scan_dash_case(operand: Bool, token: String, state: ScanState) -> Mode:
3539  match operand:
3540    case True{}: scan_settle(scan_operand(state, token))
3541    case False{}: scan_short(state, token)
3542
3543def scan_dash_token(+token: String, +state: ScanState) -> Mode:
3544  scan_dash_case(scan_hyphen_candidate(state, token), token, state)
3545
3546# How an open scan reads a token: a long token or a dash token (an option,
3547# a cluster of short options, or an operand a slot takes), or a word; "-" is
3548# a word.
3549type TokenClass is Data:
3550  LongToken{}
3551  DashToken{}
3552  WordToken{}
3553
3554def token_class(token: String) -> TokenClass:
3555  match token:
3556    case SCon{Chr{45}, SCon{Chr{45}, rest}}: LongToken{}
3557    case SCon{Chr{45}, SNil{}}: WordToken{}
3558    case SCon{Chr{45}, rest}: DashToken{}
3559    case _: WordToken{}
3560
3561def scan_token_case(kind: TokenClass, token: String,
3562    state: ScanState) -> Mode:
3563  match kind:
3564    case LongToken{}: scan_long(token, state)
3565    case DashToken{}: scan_dash_token(token, state)
3566    case WordToken{}: scan_non_option(token, state)
3567
3568def scan_token(+token: String, state: ScanState) -> Mode:
3569  scan_token_case(token_class(token), token, state)
3570
3571def scan_tail(tail: Tail, +token: String) -> Tail:
3572  match tail:
3573    case Faulted{state}: scan_faulted(state, token)
3574    case Walk{+state}:
3575      help_walk(scope_find_sub(scan_state_scope(state), token),
3576        state, token)
3577    case Stopped{state}: Stopped{state}
3578
3579# `--` ends options only here, while the scan is open (R1).
3580def scan_open(dash: Bool, state: ScanState, token: String) -> Mode:
3581  match dash:
3582    case True{}: Operands{state, True{}}
3583    case False{}: scan_token(token, state)
3584
3585# An external subcommand takes the rest of argv verbatim.
3586def scan_go(tokens: List<&2, String>, mode: Mode) -> Mode:
3587  match tokens mode:
3588    case argv Capture{state, name}:
3589      Closed{Stopped{scan_external(state, name, argv)}}
3590    case Nil{} _: mode
3591    case +token <> rest Open{state}:
3592      scan_go(rest, scan_open(String.eq(token, "--"), state, token))
3593    case token <> rest Fetch{state, info}:
3594      scan_go(rest, scan_fetch(state, info, token))
3595    case token <> rest Operands{state, ended}:
3596      scan_go(rest, Operands{scan_operand(state, token), ended})
3597    case token <> rest Closed{tail}:
3598      scan_go(rest, Closed{scan_tail(tail, token)})
3599
3600def scan_tail_close(tail: Tail) -> ScanState:
3601  match tail:
3602    case Faulted{state}: state
3603    case Walk{state}: scan_event(state, HelpHit{True{}})
3604    case Stopped{state}: state
3605
3606def scan_close(mode: Mode) -> ScanState:
3607  match mode:
3608    case Open{state}: state
3609    case Fetch{state, info}: scan_error(state, E03{arg_display(info)})
3610    case Operands{state, ended}: state
3611    case Capture{state, name}: scan_external(state, name, [])
3612    case Closed{tail}: scan_tail_close(tail)
3613
3614def scan_trace_of(state: ScanState, ended: Bool) -> ScanTrace:
3615  match state:
3616    case ScanState{scope, events, resting, positional_count,
3617        external, issue}:
3618      match scope:
3619        case Scope{path, spec}:
3620          ScanTrace{path, List.reverse(&2, ScanEvent, events),
3621            ended, external, issue}
3622
3623def scan_trace(mode: Mode) -> ScanTrace:
3624  match mode:
3625    case Operands{state, ended}: scan_trace_of(state, ended)
3626    case other: scan_trace_of(scan_close(other), False{})
3627
3628def scan(c: Cmd, argv: List<&2, String>) -> ScanTrace:
3629  scan_trace(scan_go(argv, Open{scan_start(c)}))
3630
3631def find_early(events: List<&2, ScanEvent>) -> Maybe<&2, Early>:
3632  match events:
3633    case Nil{}: None{}
3634    case HelpHit{long} <> tail: Some{EarlyHelp{long}}
3635    case VersionHit{} <> tail: Some{EarlyVersion{}}
3636    case event <> tail: find_early(tail)
3637
3638def scope_needs_sub(scope: Scope) -> Bool:
3639  match scope:
3640    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3641        external, needs_sub, version, digit_short, inherit}}: needs_sub
3642
3643def bool_text(value: Bool) -> String:
3644  Bool.pick(String, value, "true", "false")
3645
3646def gather_value(gather: Gather, +value: String,
3647    many: Bool) -> Gather:
3648  match gather:
3649    case Gather{seen, values, +count}:
3650      Bool.pick(Gather, many,
3651        Gather{True{}, value <> values, count},
3652        Gather{True{}, [value], count})
3653
3654def gather_flag(gather: Gather, value: Bool) -> Gather:
3655  match gather:
3656    case Gather{seen, values, count}:
3657      Gather{True{}, [bool_text(value)], count}
3658
3659def gather_count(gather: Gather) -> Gather:
3660  match gather:
3661    case Gather{seen, values, count}:
3662      +next = Nat.add(count, 1n)
3663      Gather{True{}, [Nat.show(next)], next}
3664
3665def kind_many(kind: UsageKind) -> Bool:
3666  match kind:
3667    case OptionKind{many}: many
3668    case PosKind{required, many}: many
3669    case _: False{}
3670
3671# gather with a hit's value, or as it was on a miss: a match, not
3672# Bool.pick, so a miss builds nothing.
3673def gather_value_if(hit: Bool, gather: Gather, value: String,
3674    kind: UsageKind) -> Gather:
3675  match hit:
3676    case True{}: gather_value(gather, value, kind_many(kind))
3677    case False{}: gather
3678
3679def gather_flag_if(hit: Bool, gather: Gather, value: Bool) -> Gather:
3680  match hit:
3681    case True{}: gather_flag(gather, value)
3682    case False{}: gather
3683
3684def gather_count_if(hit: Bool, gather: Gather) -> Gather:
3685  match hit:
3686    case True{}: gather_count(gather)
3687    case False{}: gather
3688
3689def gather_event(event: ScanEvent, id: String,
3690    kind: UsageKind, gather: Gather) -> Gather:
3691  match event:
3692    case ValueHit{key, value}:
3693      gather_value_if(same_text(key, id), gather, value, kind)
3694    case FlagHit{key, value}:
3695      gather_flag_if(same_text(key, id), gather, value)
3696    case CountHit{key}: gather_count_if(same_text(key, id), gather)
3697    case _: gather
3698
3699def gather_events(events: List<&2, ScanEvent>, +id: String,
3700    +kind: UsageKind, gather: Gather) -> Gather:
3701  match events:
3702    case Nil{}: gather
3703    case event <> tail:
3704      gather_events(tail, id, kind,
3705        gather_event(event, id, kind, gather))
3706
3707def gather_arg(events: List<&2, ScanEvent>, info: ArgInfo) -> Gather:
3708  match info:
3709    case ArgInfo{id, usage, short, long, kind, attrs}:
3710      gather_events(events, id, kind, Gather{False{}, [], 0n})
3711
3712# Each env() name, in order.
3713def find_envs(attrs: List<&2, Item>) -> List<&2, String>:
3714  match attrs:
3715    case Nil{}: Nil{}
3716    case AttrEnv{name} <> tail: name <> find_envs(tail)
3717    case head <> tail: find_envs(tail)
3718
3719def env_present(name: String, found: Maybe<&2, String>) ->
3720    Maybe<&2, EnvValue>:
3721  match found:
3722    case None{}: None{}
3723    case Some{+value}:
3724      Bool.pick(Maybe<&2, EnvValue>, String.is_empty(value),
3725        None{}, Some{EnvValue{name, value}})
3726
3727# found, or else name's value in vars if it is set and not empty.
3728def env_next(found: Maybe<&2, EnvValue>, +name: String,
3729    vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3730  match found:
3731    case Some{env}: Some{env}
3732    case None{}: env_present(name, var_value(vars, name))
3733
3734# found, or else the first of names that is set and not empty in vars.
3735def env_first(names: List<&2, String>, found: Maybe<&2, EnvValue>,
3736    +vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3737  match names:
3738    case Nil{}: found
3739    case name <> tail: env_first(tail, env_next(found, name, vars), vars)
3740
3741def host_env(h: Host, names: List<&2, String>) -> Maybe<&2, EnvValue>:
3742  match h:
3743    case Host{width, vars, out_tty, err_tty}:
3744      env_first(names, None{}, vars)
3745
3746def env_bool_true(value: String) -> Bool:
3747  List.contains(~String, ~String.eq,
3748    ["true", "yes", "on", "1"], String.to_lower(value))
3749
3750def env_values(kind: UsageKind, attrs: List<&2, Item>,
3751    value: String) -> List<&2, String>:
3752  match kind:
3753    case FlagKind{}: [bool_text(env_bool_true(value))]
3754    case NegatableKind{}: [bool_text(env_bool_true(value))]
3755    case CountKind{}: [value]
3756    case _: apply_delim(find_delim(attrs), [value])
3757
3758def resolve_env_valid(found: Maybe<&2, Bad>, info: ArgInfo,
3759    name: String, value: String) -> FieldBuild:
3760  match found:
3761    case Some{Bad{bad, why}}:
3762      BuildFault{E11{arg_display(info), bad, name, why}}
3763    case None{}:
3764      match info:
3765        case ArgInfo{id, usage, short, long, kind, attrs}:
3766          Built{[Field{id, env_values(kind, attrs, value), Env{}}]}
3767
3768def resolve_env(+info: ArgInfo, env: EnvValue) -> FieldBuild:
3769  match env:
3770    case EnvValue{name, +value}:
3771      resolve_env_valid(value_issue(info_kind(info), info_attrs(info), value),
3772        info, name, value)
3773
3774def resolve_default(found: Maybe<&2, String>,
3775    info: ArgInfo) -> FieldBuild:
3776  match found:
3777    case None{}: Built{[]}
3778    case Some{value}:
3779      match info:
3780        case ArgInfo{id, usage, short, long, kind, attrs}:
3781          Built{[Field{id, env_values(kind, attrs, value), Default{}}]}
3782
3783def info_default(info: ArgInfo) -> Maybe<&2, String>:
3784  match info:
3785    case ArgInfo{id, usage, short, long, kind, attrs}:
3786      find_default(attrs)
3787
3788def resolve_env_default(found: Maybe<&2, EnvValue>,
3789    +info: ArgInfo) -> FieldBuild:
3790  match found:
3791    case Some{env}: resolve_env(info, env)
3792    case None{}: resolve_default(info_default(info), info)
3793
3794def info_envs(info: ArgInfo) -> List<&2, String>:
3795  match info:
3796    case ArgInfo{id, usage, short, long, kind, attrs}:
3797      find_envs(attrs)
3798
3799def resolve_argv_gather(gather: Gather, +info: ArgInfo,
3800    h: Host) -> FieldBuild:
3801  match gather:
3802    case Gather{True{}, values, count}:
3803      match info:
3804        case ArgInfo{id, usage, short, long, kind, attrs}:
3805          Built{[Field{id, apply_delim(find_delim(attrs),
3806            List.reverse(&2, String, values)), Argv{}}]}
3807    case Gather{False{}, values, count}:
3808      resolve_env_default(host_env(h, info_envs(info)), info)
3809
3810def resolve_option(+info: ArgInfo,
3811    events: List<&2, ScanEvent>, h: Host) -> FieldBuild:
3812  resolve_argv_gather(gather_arg(events, info), info, h)
3813
3814def scope_arg_items(scope: Scope) -> List<&2, SpecItem>:
3815  match scope:
3816    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3817        external, needs_sub, version, digit_short, inherit}}: args
3818
3819def combine_built(fields: List<&2, Field>,
3820    next: FieldBuild) -> FieldBuild:
3821  match next:
3822    case Built{more}:
3823      Built{List.append(&2, Field, fields, more)}
3824    case BuildFault{fault}: BuildFault{fault}
3825
3826def combine_build(first: FieldBuild,
3827    second: FieldBuild) -> FieldBuild:
3828  match first:
3829    case Built{fields}: combine_built(fields, second)
3830    case BuildFault{fault}: BuildFault{fault}
3831
3832def resolve_arg(pos: Bool, +info: ArgInfo, events: List<&2, ScanEvent>,
3833    h: Host, later: FieldBuild) -> FieldBuild:
3834  match pos:
3835    case True{}: later
3836    case False{}: combine_build(resolve_option(info, events, h), later)
3837
3838def resolve_options(items: List<&2, SpecItem>,
3839    +events: List<&2, ScanEvent>, +h: Host) -> FieldBuild:
3840  match items:
3841    case Nil{}: Built{[]}
3842    case SpecArg{+info} <> tail:
3843      resolve_arg(kind_pos(info_kind(info)), info, events, h,
3844        resolve_options(tail, events, h))
3845    case head <> tail: resolve_options(tail, events, h)
3846
3847def scan_operands(events: List<&2, ScanEvent>) ->
3848    List<&2, String>:
3849  match events:
3850    case Nil{}: Nil{}
3851    case Operand{token} <> tail:
3852      token <> scan_operands(tail)
3853    case event <> tail: scan_operands(tail)
3854
3855def position_take_count(kind: UsageKind,
3856    after: List<&2, ArgInfo>, +tokens: List<&2, String>) -> Nat:
3857  match kind:
3858    case PosKind{required, True{}}:
3859      Nat.sub(List.length(&2, String, tokens),
3860        List.length(&2, ArgInfo, after))
3861    case _:
3862      Bool.pick(Nat, List.is_empty(&2, String, tokens), 0n, 1n)
3863
3864def pos_valid_result(found: Maybe<&2, Bad>,
3865    info: ArgInfo, values: List<&2, String>) -> FieldBuild:
3866  match found:
3867    case Some{Bad{bad, why}}:
3868      BuildFault{E04{arg_display(info), bad, why}}
3869    case None{}:
3870      match info:
3871        case ArgInfo{id, usage, short, long, kind, attrs}:
3872          Built{[Field{id, values, Argv{}}]}
3873
3874def resolve_pos(+info: ArgInfo,
3875    +values: List<&2, String>, h: Host) -> FieldBuild:
3876  match values:
3877    case Nil{}:
3878      resolve_env_default(host_env(h, info_envs(info)), info)
3879    case head <> tail:
3880      +attrs = info_attrs(info)
3881      +split = apply_delim(find_delim(attrs), values)
3882      pos_valid_result(first_bad(split, attrs), info, split)
3883
3884def resolve_positions(slots: List<&2, ArgInfo>,
3885    +tokens: List<&2, String>, +h: Host) -> FieldBuild:
3886  match slots:
3887    case Nil{}: Built{[]}
3888    case +info <> +tail:
3889      +amount = position_take_count(info_kind(info), tail, tokens)
3890      combine_build(
3891        resolve_pos(info, List.take(&2, String, tokens, amount), h),
3892        resolve_positions(tail, List.drop(&2, String, tokens, amount), h))
3893
3894def scope_positions(scope: Scope) -> List<&2, ArgInfo>:
3895  match scope:
3896    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3897        external, needs_sub, version, digit_short, inherit}}: slots
3898
3899def resolve_scope(+scope: Scope, +events: List<&2, ScanEvent>,
3900    +h: Host) -> FieldBuild:
3901  combine_build(
3902    resolve_options(scope_arg_items(scope), events, h),
3903    resolve_positions(scope_positions(scope),
3904      scan_operands(events), h))
3905
3906def source_given(source: Src) -> Bool:
3907  match source:
3908    case Argv{}: True{}
3909    case Env{}: True{}
3910    case Default{}: False{}
3911    case Absent{}: False{}
3912
3913def field_source(fields: List<&2, Field>, +id: String) -> Src:
3914  match fields:
3915    case Nil{}: Absent{}
3916    case Field{key, values, source} <> tail:
3917      Bool.pick(Src, String.eq(key, id), source,
3918        field_source(tail, id))
3919
3920def field_given(fields: List<&2, Field>, id: String) -> Bool:
3921  source_given(field_source(fields, id))
3922
3923def source_present(source: Src) -> Bool:
3924  match source:
3925    case Absent{}: False{}
3926    case _: True{}
3927
3928def field_present(fields: List<&2, Field>, id: String) -> Bool:
3929  source_present(field_source(fields, id))
3930
3931def group_usages(items: List<&2, Item>, +name: String) ->
3932    List<&2, String>:
3933  match items:
3934    case Nil{}: Nil{}
3935    case Arg{+usage, help, +attrs} <> tail:
3936      +later = group_usages(tail, name)
3937      Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3938        arg_display(arg_info(usage, attrs)) <> later, later)
3939    case head <> tail: group_usages(tail, name)
3940
3941def spec_group_usages(items: List<&2, SpecItem>, +name: String) ->
3942    List<&2, String>:
3943  match items:
3944    case Nil{}: Nil{}
3945    case SpecArg{+info} <> tail:
3946      +later = spec_group_usages(tail, name)
3947      attrs = info_attrs(info)
3948      Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3949        arg_display(info) <> later, later)
3950    case head <> tail: spec_group_usages(tail, name)
3951
3952def spec_group_display(items: List<&2, SpecItem>, name: String) -> String:
3953  String.concat(["<", String.join(spec_group_usages(items, name), "|"), ">"])
3954
3955def group_given(items: List<&2, SpecItem>,
3956    +fields: List<&2, Field>, +name: String) -> Bool:
3957  match items:
3958    case Nil{}: False{}
3959    case SpecArg{ArgInfo{id, usage, short, long, kind, attrs}} <> tail:
3960      Bool.or(Bool.and(arg_in_group(attrs, name), field_given(fields, id)),
3961        group_given(tail, fields, name))
3962    case head <> tail: group_given(tail, fields, name)
3963
3964def arg_required_missing(info: ArgInfo,
3965    +fields: List<&2, Field>) -> Bool:
3966  match info:
3967    case ArgInfo{+id, usage, short, long, kind, attrs}:
3968      Bool.or(Bool.and(has_required(attrs),
3969        Bool.not(field_given(fields, id))),
3970        Bool.and(kind_required_pos(kind),
3971          Bool.not(field_present(fields, id))))
3972
3973def group_display(items: List<&2, Item>, name: String) -> String:
3974  String.concat(["<", String.join(group_usages(items, name), "|"), ">"])
3975
3976def missing_arg(+info: ArgInfo,
3977    fields: List<&2, Field>) -> List<&2, String>:
3978  Bool.pick(List<&2, String>,
3979    arg_required_missing(info, fields),
3980    [arg_display(info)], [])
3981
3982def missing_group(+items: List<&2, SpecItem>,
3983    fields: List<&2, Field>, +name: String) -> List<&2, String>:
3984  Bool.pick(List<&2, String>,
3985    group_given(items, fields, name), [],
3986    [spec_group_display(items, name)])
3987
3988# A group's display the first time a need_one rule names it.
3989def missing_once(listed: Bool, items: List<&2, SpecItem>,
3990    fields: List<&2, Field>, name: String) -> List<&2, String>:
3991  match listed:
3992    case True{}: []
3993    case False{}: missing_group(items, fields, name)
3994
3995# What E06 lists, in items' order: each required arg not given, and each
3996# need_one group with no arg given that done does not name yet.
3997def missing_items(items: List<&2, SpecItem>,
3998    +all_items: List<&2, SpecItem>,
3999    +fields: List<&2, Field>, +done: List<&2, String>) ->
4000    List<&2, String>:
4001  match items:
4002    case Nil{}: Nil{}
4003    case SpecArg{info} <> tail:
4004      List.append(&2, String,
4005        missing_arg(info, fields),
4006        missing_items(tail, all_items, fields, done))
4007    case SpecOther{NeedOne{+name}} <> tail:
4008      List.append(&2, String,
4009        missing_once(List.contains(~String, ~String.eq, done, name),
4010          all_items, fields, name),
4011        missing_items(tail, all_items, fields, name <> done))
4012    case head <> tail:
4013      missing_items(tail, all_items, fields, done)
4014
4015def scope_missing(scope: Scope,
4016    fields: List<&2, Field>) -> List<&2, String>:
4017  +items = scope_arg_items(scope)
4018  missing_items(items, items, fields, [])
4019
4020def append_new(seen: Bool, ids: List<&2, String>, id: String) ->
4021    List<&2, String>:
4022  match seen:
4023    case True{}: ids
4024    case False{}: List.append(&2, String, ids, [id])
4025
4026# Whether names holds query: List.contains with String.eq would copy each
4027# name it compares.
4028def has_text(names: List<&2, String>, +query: String, found: Bool) -> Bool:
4029  match names found:
4030    case _ True{}: True{}
4031    case Nil{} False{}: False{}
4032    case name <> tail False{}: has_text(tail, query, same_text(name, query))
4033
4034def append_unique(+ids: List<&2, String>, +id: String) ->
4035    List<&2, String>:
4036  append_new(has_text(ids, id, False{}), ids, id)
4037
4038def append_event_id(found: Maybe<&2, String>,
4039    ids: List<&2, String>) -> List<&2, String>:
4040  match found:
4041    case None{}: ids
4042    case Some{id}: append_unique(ids, id)
4043
4044def argv_order(events: List<&2, ScanEvent>,
4045    ids: List<&2, String>) -> List<&2, String>:
4046  match events:
4047    case Nil{}: ids
4048    case event <> tail:
4049      argv_order(tail, append_event_id(event_id(event), ids))
4050
4051def source_env(source: Src) -> Bool:
4052  match source:
4053    case Env{}: True{}
4054    case _: False{}
4055
4056def env_order(fields: List<&2, Field>,
4057    +ids: List<&2, String>) -> List<&2, String>:
4058  match fields:
4059    case Nil{}: ids
4060    case Field{id, values, source} <> tail:
4061      env_order(tail, Bool.pick(List<&2, String>,
4062        source_env(source), append_unique(ids, id), ids))
4063
4064def present_order(events: List<&2, ScanEvent>,
4065    fields: List<&2, Field>) -> List<&2, String>:
4066  env_order(fields, argv_order(events, []))
4067
4068def arg_info_found(item: Item, id: String) -> Maybe<&2, ArgInfo>:
4069  match item:
4070    case Arg{usage, help, attrs}:
4071      +info = arg_info(usage, attrs)
4072      own = info_id(info)
4073      Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
4074    case _: None{}
4075
4076# The first Arg whose id is id; after a hit it walks no further.
4077def find_arg_go(items: List<&2, Item>, +id: String,
4078    found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
4079  match items found:
4080    case _ Some{hit}: Some{hit}
4081    case Nil{} None{}: None{}
4082    case item <> tail None{}: find_arg_go(tail, id, arg_info_found(item, id))
4083
4084def find_arg_info(items: List<&2, Item>, +id: String) ->
4085    Maybe<&2, ArgInfo>:
4086  find_arg_go(items, id, None{})
4087
4088def exclusive_groups(items: List<&2, SpecItem>) -> List<&2, String>:
4089  match items:
4090    case Nil{}: Nil{}
4091    case SpecOther{Exclusive{name}} <> tail:
4092      name <> exclusive_groups(tail)
4093    case head <> tail: exclusive_groups(tail)
4094
4095def common_exclusive(groups: List<&2, String>,
4096    +left: List<&2, String>, +right: List<&2, String>) -> Bool:
4097  match groups:
4098    case Nil{}: False{}
4099    case +name <> tail:
4100      Bool.or(Bool.and(
4101        List.contains(~String, ~String.eq, left, name),
4102        List.contains(~String, ~String.eq, right, name)),
4103        common_exclusive(tail, left, right))
4104
4105def pair_conflicts(+later: ArgInfo, +earlier: ArgInfo,
4106    groups: List<&2, String>) -> Bool:
4107  +later_attrs = info_attrs(later)
4108  +earlier_attrs = info_attrs(earlier)
4109  Bool.or(List.contains(~String, ~String.eq,
4110      find_conflicts(later_attrs), info_id(earlier)),
4111    Bool.or(List.contains(~String, ~String.eq,
4112        find_conflicts(earlier_attrs), info_id(later)),
4113      common_exclusive(groups,
4114        find_group_attrs(later_attrs),
4115        find_group_attrs(earlier_attrs))))
4116
4117def pair_fault(+later: ArgInfo, earlier: Maybe<&2, ArgInfo>,
4118    groups: List<&2, String>) -> Maybe<&2, ScanFault>:
4119  match earlier:
4120    case None{}: None{}
4121    case Some{+prior}:
4122      Bool.pick(Maybe<&2, ScanFault>,
4123        pair_conflicts(later, prior, groups),
4124        Some{E07{arg_display(later), arg_display(prior)}}, None{})
4125
4126def earlier_conflict(earlier_ids: List<&2, String>,
4127    +later: ArgInfo, +items: List<&2, SpecItem>,
4128    +groups: List<&2, String>,
4129    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4130  match earlier_ids found:
4131    case Nil{} Some{fault}: Some{fault}
4132    case Nil{} None{}: None{}
4133    case id <> tail Some{fault}: Some{fault}
4134    case id <> tail None{}:
4135      earlier_conflict(tail, later, items, groups,
4136        pair_fault(later, find_spec_info(items, id, None{}), groups))
4137
4138def conflict_for_id(info: Maybe<&2, ArgInfo>,
4139    seen: List<&2, String>, items: List<&2, SpecItem>,
4140    groups: List<&2, String>) -> Maybe<&2, ScanFault>:
4141  match info:
4142    case None{}: None{}
4143    case Some{later}:
4144      earlier_conflict(seen, later, items, groups, None{})
4145
4146def conflict_go(ids: List<&2, String>,
4147    +seen: List<&2, String>, +items: List<&2, SpecItem>,
4148    +groups: List<&2, String>,
4149    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4150  match ids found:
4151    case Nil{} Some{fault}: Some{fault}
4152    case Nil{} None{}: None{}
4153    case id <> tail Some{fault}: Some{fault}
4154    case +id <> tail None{}:
4155      conflict_go(tail, List.append(&2, String, seen, [id]),
4156        items, groups,
4157        conflict_for_id(find_spec_info(items, id, None{}),
4158          seen, items, groups))
4159
4160def require_target_fault(target: Maybe<&2, ArgInfo>,
4161    +requester: ArgInfo, fields: List<&2, Field>) ->
4162    Maybe<&2, ScanFault>:
4163  match target:
4164    case None{}: None{}
4165    case Some{+needed}:
4166      Bool.pick(Maybe<&2, ScanFault>,
4167        Bool.not(field_given(fields, info_id(needed))),
4168        Some{E08{arg_display(requester), arg_display(needed)}},
4169        None{})
4170
4171def require_targets(targets: List<&2, String>,
4172    +requester: ArgInfo, +items: List<&2, SpecItem>,
4173    +fields: List<&2, Field>,
4174    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4175  match targets found:
4176    case Nil{} Some{fault}: Some{fault}
4177    case Nil{} None{}: None{}
4178    case name <> tail Some{fault}: Some{fault}
4179    case name <> tail None{}:
4180      require_targets(tail, requester, items, fields,
4181        require_target_fault(find_spec_info(items, name, None{}),
4182          requester, fields))
4183
4184def require_for_id(info: Maybe<&2, ArgInfo>,
4185    items: List<&2, SpecItem>,
4186    fields: List<&2, Field>) -> Maybe<&2, ScanFault>:
4187  match info:
4188    case None{}: None{}
4189    case Some{+requester}:
4190      require_targets(find_requires(info_attrs(requester)),
4191        requester, items, fields, None{})
4192
4193def require_go(ids: List<&2, String>,
4194    +items: List<&2, SpecItem>, +fields: List<&2, Field>,
4195    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4196  match ids found:
4197    case Nil{} Some{fault}: Some{fault}
4198    case Nil{} None{}: None{}
4199    case id <> tail Some{fault}: Some{fault}
4200    case id <> tail None{}:
4201      require_go(tail, items, fields,
4202        require_for_id(find_spec_info(items, id, None{}), items, fields))
4203
4204def parse_requires(fault: Maybe<&2, ScanFault>,
4205    path: List<&2, String>, fields: List<&2, Field>,
4206    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4207  match fault:
4208    case Some{problem}: Rejected{path, problem}
4209    case None{}: Accepted{path, fields, external}
4210
4211def parse_conflicts(fault: Maybe<&2, ScanFault>,
4212    +scope: Scope, +fields: List<&2, Field>,
4213    events: List<&2, ScanEvent>,
4214    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4215  match fault:
4216    case Some{problem}: Rejected{scope_path(scope), problem}
4217    case None{}:
4218      parse_requires(require_go(
4219          present_order(events, fields),
4220          scope_arg_items(scope), fields, None{}),
4221        scope_path(scope), fields, external)
4222
4223def parse_missing(missing: List<&2, String>,
4224    +scope: Scope, +fields: List<&2, Field>,
4225    +events: List<&2, ScanEvent>,
4226    empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4227  match missing:
4228    case Nil{}:
4229      +items = scope_arg_items(scope)
4230      parse_conflicts(conflict_go(present_order(events, fields),
4231          [], items, exclusive_groups(items), None{}),
4232        scope, fields, events, external)
4233    case head <> tail:
4234      Bool.pick(ParseResult, empty, SummaryRequired{},
4235        Rejected{scope_path(scope), E06{head <> tail}})
4236
4237def parse_built(build: FieldBuild, +scope: Scope,
4238    events: List<&2, ScanEvent>, empty: Bool,
4239    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4240  match build:
4241    case BuildFault{fault}: Rejected{scope_path(scope), fault}
4242    case Built{+fields}:
4243      parse_missing(scope_missing(scope, fields),
4244        scope, fields, events, empty, external)
4245
4246def parse_no_issue(needs_sub: Bool, +scope: Scope,
4247    +events: List<&2, ScanEvent>, +h: Host,
4248    +empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4249  Bool.pick(ParseResult, needs_sub,
4250    Bool.pick(ParseResult, empty, SummaryRequired{},
4251      Rejected{scope_path(scope), E10{}}),
4252    parse_built(resolve_scope(scope, events, h),
4253      scope, events, empty, external))
4254
4255def parse_issue(issue: Maybe<&2, ScanFault>,
4256    +scope: Scope, events: List<&2, ScanEvent>,
4257    h: Host, empty: Bool,
4258    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4259  match issue:
4260    case Some{fault}: Rejected{scope_path(scope), fault}
4261    case None{}:
4262      parse_no_issue(scope_needs_sub(scope), scope,
4263        events, h, empty, external)
4264
4265def parse_early(found: Maybe<&2, Early>,
4266    +scope: Scope, events: List<&2, ScanEvent>, h: Host,
4267    empty: Bool, issue: Maybe<&2, ScanFault>,
4268    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4269  match found:
4270    case Some{EarlyHelp{long}}:
4271      HelpRequested{scope_path(scope), long}
4272    case Some{EarlyVersion{}}:
4273      VersionRequested{}
4274    case None{}:
4275      parse_issue(issue, scope, events, h, empty, external)
4276
4277def parse_from_state(state: ScanState, h: Host,
4278    empty: Bool) -> ParseResult:
4279  match state:
4280    case ScanState{scope, events, resting, positional_count,
4281        external, issue}:
4282      +ordered: List<&2, ScanEvent> =
4283        List.reverse(&2, ScanEvent, events)
4284      parse_early(find_early(ordered), scope, ordered, h,
4285        empty, issue, external)
4286
4287def parse(c: Cmd, h: Host, +argv: List<&2, String>) -> ParseResult:
4288  parse_from_state(scan_close(scan_go(argv, Open{scan_start(c)})),
4289    h, List.is_empty(&2, String, argv))
4290
4291# ---- Reading values (§6.5) ----
4292
4293type Fault is Data:
4294  Bug{msg: String}
4295  Usage{msg: String}
4296
4297def Dec(-A: Type) -> Type:
4298  Result<&2, &1, Fault, A>
4299
4300def Dec.pure(-A: Type, x: A) -> Dec(A):
4301  Done{x}
4302
4303def Dec.bind(-A: Type, -B: Type, r: Dec(A), f: A -> Dec(B)) -> Dec(B):
4304  match r:
4305    case Fail{e}: Fail{e}
4306    case Done{x}: f(x)
4307
4308type Matches is Data:
4309  Matches{cmd: Cmd, path: List<&2, String>, fields: List<&2, Field>,
4310    external: Maybe<&2, ExternalArgs>}
4311
4312def matches(c: Cmd, r: ParseResult) -> Maybe<&2, Matches>:
4313  match r:
4314    case Accepted{path, fields, external}:
4315      Some{Matches{c, path, fields, external}}
4316    case _: None{}
4317
4318def path(m: Matches) -> List<&2, String>:
4319  match m:
4320    case Matches{cmd, path, fields, external}: path
4321
4322def external_args(m: Matches) -> Maybe<(String & List<&2, String>)>:
4323  match m:
4324    case Matches{cmd, path, fields, None{}}: None{}
4325    case Matches{cmd, path, fields, Some{ExternalArgs{name, args}}}:
4326      Some{(name, args)}
4327
4328def fail(-A: Type, msg: String) -> Dec(A):
4329  Fail{Usage{msg}}
4330
4331def bug(-A: Type, msg: String) -> Dec(A):
4332  Fail{Bug{msg}}
4333
4334def keep(+m: Matches) -> Dec(Matches):
4335  Done{m}
4336
4337# A command path as lint writes it: the root name, then each subcommand.
4338def path_text(name: String, path: List<&2, String>) -> String:
4339  String.join(name <> path, " ")
4340
4341def sub_items(found: Maybe<&2, SubFound>) -> List<&2, Item>:
4342  match found:
4343    case None{}: Nil{}
4344    case Some{SubFound{name, items}}: items
4345
4346# The args in scope at path (§8.1), as parse orders them: globals first.
4347def scope_args(path: List<&2, String>, +items: List<&2, Item>,
4348    globals: List<&2, Item>) -> List<&2, Item>:
4349  match path:
4350    case Nil{}: List.append(&2, Item, globals, items)
4351    case name <> tail:
4352      scope_args(tail, sub_items(find_sub(items, name)),
4353        List.append(&2, Item, global_items(items), globals))
4354
4355type Form is Data:
4356  ReadOne{}
4357  ReadOpt{}
4358  ReadMany{}
4359
4360# What a reader asks of an arg; Q-static checks it against the spec.
4361type Want is Data:
4362  WFlag{}
4363  WCount{}
4364  WValue{stem: String, form: Form}
4365  WAny{}
4366  WChoose{opt: Bool, keys: List<&2, String>}
4367  WProgram{opt: Bool}
4368
4369def kind_flag(kind: UsageKind) -> Bool:
4370  match kind:
4371    case FlagKind{}: True{}
4372    case NegatableKind{}: True{}
4373    case _: False{}
4374
4375def kind_count(kind: UsageKind) -> Bool:
4376  match kind:
4377    case CountKind{}: True{}
4378    case _: False{}
4379
4380# The reader name a value type reads with: str, nat, u32, f32 or bool.
4381def type_stem(found: Maybe<&2, ValueType>) -> String:
4382  match found:
4383    case Some{TNat{}}: "nat"
4384    case Some{TU32{}}: "u32"
4385    case Some{TF32{}}: "f32"
4386    case Some{TBool{}}: "bool"
4387    case _: "str"
4388
4389def stem_noun(+stem: String) -> String:
4390  Bool.pick(String, String.eq(stem, "str"), "string", stem)
4391
4392def stem_article(+stem: String) -> String:
4393  Bool.pick(String, String.eq(stem, "f32"), "an ", "a ")
4394
4395def always_present(kind: UsageKind, +attrs: List<&2, Item>) -> Bool:
4396  Bool.or(has_required(attrs), Bool.or(
4397    Maybe.is_some(&2, String, find_default(attrs)), kind_required_pos(kind)))
4398
4399def may_absent(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4400  Bool.and(Bool.not(kind_many(kind)),
4401    Bool.not(always_present(kind, attrs)))
4402
4403def kind_text(kind: UsageKind, +stem: String) -> String:
4404  match kind:
4405    case FlagKind{}: "a flag"
4406    case NegatableKind{}: "a flag"
4407    case CountKind{}: "a count"
4408    case _:
4409      Bool.pick(String, kind_many(kind),
4410        String.concat(["a list of ", stem_noun(stem), " values"]),
4411        String.concat([stem_article(stem), stem_noun(stem), " value"]))
4412
4413def fit_name(+kind: UsageKind, attrs: List<&2, Item>,
4414    +stem: String) -> String:
4415  match kind:
4416    case FlagKind{}: "flag"
4417    case NegatableKind{}: "flag"
4418    case CountKind{}: "count"
4419    case _:
4420      Bool.pick(String, kind_many(kind), String.append(stem, "s"),
4421        Bool.pick(String, always_present(kind, attrs), stem,
4422          String.append(stem, "_opt")))
4423
4424def kind_verdict(fits: Bool, id: String, +kind: UsageKind,
4425    +attrs: List<&2, Item>) -> Maybe<&2, String>:
4426  match fits:
4427    case True{}: None{}
4428    case False{}:
4429      +stem = type_stem(find_as(attrs))
4430      Some{String.concat(["'", id, "' is ", kind_text(kind, stem),
4431        "; read it with Cli.", fit_name(kind, attrs, stem)])}
4432
4433def absent_verdict(absent: Bool, id: String,
4434    reader: String) -> Maybe<&2, String>:
4435  match absent:
4436    case True{}:
4437      Some{String.concat(["'", id, "' may be absent; read it with Cli.",
4438        reader, "_opt"])}
4439    case False{}: None{}
4440
4441def all_in(xs: List<&2, String>, +ys: List<&2, String>) -> Bool:
4442  match xs:
4443    case Nil{}: True{}
4444    case head <> tail:
4445      Bool.and(List.contains(~String, ~String.eq, ys, head),
4446        all_in(tail, ys))
4447
4448def keys_issue(same: Bool, id: String) -> Maybe<&2, String>:
4449  match same:
4450    case True{}: None{}
4451    case False{}:
4452      Some{String.concat(["choose table for '", id,
4453        "' does not match its values"])}
4454
4455def keys_verdict(found: Maybe<&2, List<&2, String>>,
4456    +keys: List<&2, String>, id: String) -> Maybe<&2, String>:
4457  match found:
4458    case None{}: None{}
4459    case Some{+values}:
4460      keys_issue(Bool.and(Bool.not(List.is_empty(&2, String, keys)),
4461        Bool.and(all_in(keys, values), all_in(values, keys))), id)
4462
4463def form_fits(form: Form, many: Bool) -> Bool:
4464  match form:
4465    case ReadMany{}: many
4466    case _: Bool.not(many)
4467
4468def form_one(form: Form) -> Bool:
4469  match form:
4470    case ReadOne{}: True{}
4471    case _: False{}
4472
4473def value_fits(stem: String, form: Form, +kind: UsageKind,
4474    attrs: List<&2, Item>) -> Bool:
4475  Bool.and(kind_valued(kind), Bool.and(
4476    String.eq(stem, type_stem(find_as(attrs))),
4477    form_fits(form, kind_many(kind))))
4478
4479def choose_fits(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4480  Bool.and(kind_valued(kind), Bool.and(Bool.not(kind_many(kind)),
4481    Maybe.is_some(&2, List<&2, String>, find_one_of(attrs))))
4482
4483# A program and its arguments come from one variadic string positional.
4484def program_fits(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4485  Bool.and(kind_pos(kind), value_fits("str", ReadMany{}, kind, attrs))
4486
4487def rest_verdict(rest: Bool, id: String) -> Maybe<&2, String>:
4488  match rest:
4489    case True{}: None{}
4490    case False{}:
4491      Some{String.concat(["'", id,
4492        "' has no Cli.rest(); give it one or read it with Cli.strs"])}
4493
4494# The first Q-static message for an arg in scope: kind, absence, then keys,
4495# or for a program, then rest().
4496def want_verdict(want: Want, +id: String, +kind: UsageKind,
4497    +attrs: List<&2, Item>) -> Maybe<&2, String>:
4498  match want:
4499    case WFlag{}:
4500      kind_verdict(kind_flag(kind), id, kind, attrs)
4501    case WCount{}:
4502      kind_verdict(kind_count(kind), id, kind, attrs)
4503    case WValue{+stem, +form}:
4504      Maybe.or(&2, String,
4505        kind_verdict(value_fits(stem, form, kind, attrs), id, kind, attrs),
4506        absent_verdict(Bool.and(form_one(form), may_absent(kind, attrs)),
4507          id, stem))
4508    case WAny{}:
4509      None{}
4510    case WChoose{opt, keys}:
4511      Maybe.or(&2, String,
4512        kind_verdict(choose_fits(kind, attrs), id, kind, attrs),
4513        Maybe.or(&2, String,
4514          absent_verdict(Bool.and(Bool.not(opt), may_absent(kind, attrs)),
4515            id, "choose"),
4516          keys_verdict(find_one_of(attrs), keys, id)))
4517    case WProgram{opt}:
4518      Maybe.or(&2, String,
4519        kind_verdict(program_fits(kind, attrs), id, kind, attrs),
4520        Maybe.or(&2, String,
4521          absent_verdict(Bool.and(Bool.not(opt),
4522            Bool.not(always_present(kind, attrs))), id, "program"),
4523          rest_verdict(has_rest(attrs), id)))
4524
4525def arg_verdict(found: Maybe<&2, ArgInfo>, id: String, name: String,
4526    path: List<&2, String>, want: Want) -> Maybe<&2, String>:
4527  match found:
4528    case None{}:
4529      Some{String.concat(["'", id, "' is not an argument of '",
4530        path_text(name, path), "'"])}
4531    case Some{ArgInfo{key, usage, short, long, kind, attrs}}:
4532      want_verdict(want, id, kind, attrs)
4533
4534# Q-static: depends only on the spec, the path, the id and the want.
4535def verdict(cmd: Cmd, +path: List<&2, String>, +id: String,
4536    want: Want) -> Maybe<&2, String>:
4537  match cmd:
4538    case Cmd{name, about, items}:
4539      arg_verdict(find_arg_info(scope_args(path, items, []), id), id, name,
4540        path, want)
4541
4542def field_found(f: Field, id: String) -> Maybe<&2, Field>:
4543  match f:
4544    case Field{+key, values, source}:
4545      Bool.pick(Maybe<&2, Field>, same_text(key, id),
4546        Some{Field{key, values, source}}, None{})
4547
4548# The field for id, or an absent one; after a hit it walks no further.
4549def field_seek(fields: List<&2, Field>, +id: String,
4550    found: Maybe<&2, Field>) -> Field:
4551  match fields found:
4552    case _ Some{f}: f
4553    case Nil{} None{}: Field{id, [], Absent{}}
4554    case f <> tail None{}: field_seek(tail, id, field_found(f, id))
4555
4556def field_find(fields: List<&2, Field>, +id: String) -> Field:
4557  field_seek(fields, id, None{})
4558
4559# Validation first; reading a validated value never fails (§6.5).
4560def read_check(~T: Data, ~get: Field -> T, verdict: Maybe<&2, String>,
4561    fields: List<&2, Field>, id: String) -> Dec(T):
4562  match verdict:
4563    case None{}: Done{get(field_find(fields, id))}
4564    case Some{msg}: Fail{Bug{msg}}
4565
4566def read_with(~T: Data, ~get: Field -> T, want: Want, m: Matches,
4567    +id: String) -> Dec(T):
4568  match m:
4569    case Matches{cmd, path, fields, external}:
4570      read_check(~T, ~get, verdict(cmd, path, id, want), fields, id)
4571
4572def get_one(~T: Data, ~conv: String -> T, f: Field) -> T:
4573  match f:
4574    case Field{id, head <> tail, source}: conv(head)
4575    case _: conv("")
4576
4577def get_opt(~T: Data, ~conv: String -> T, f: Field) -> Maybe<&2, T>:
4578  match f:
4579    case Field{id, head <> tail, source}: Some{conv(head)}
4580    case _: None{}
4581
4582def convert_all(~T: Data, ~conv: String -> T, values: List<&2, String>) ->
4583    List<&2, T>:
4584  match values:
4585    case Nil{}: Nil{}
4586    case head <> tail: conv(head) <> convert_all(~T, ~conv, tail)
4587
4588def get_many(~T: Data, ~conv: String -> T, f: Field) -> List<&2, T>:
4589  match f:
4590    case Field{id, values, source}: convert_all(~T, ~conv, values)
4591
4592def true_of(s: String) -> Bool:
4593  String.eq(s, "true")
4594
4595def nat_of(s: String) -> Nat:
4596  Maybe.default(&2, Nat, Nat.read(s), 0n)
4597
4598def u32_of(s: String) -> U32:
4599  Maybe.default(&2, U32, U32.read(s), 0)
4600
4601def f32_of(s: String) -> F32:
4602  Maybe.default(&2, F32, F32.read(s), 0.0)
4603
4604def flag(m: Matches, id: String) -> Dec(Bool):
4605  read_with(~Bool, ~(f => get_one(~Bool, ~true_of, f)), WFlag{}, m, id)
4606
4607def count(m: Matches, id: String) -> Dec(Nat):
4608  read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4609    WCount{}, m, id)
4610
4611def str(m: Matches, id: String) -> Dec(String):
4612  read_with(~String, ~(f => get_one(~String, ~(s => s), f)),
4613    WValue{"str", ReadOne{}}, m, id)
4614
4615def str_opt(m: Matches, id: String) -> Dec(Maybe<&2, String>):
4616  read_with(~Maybe<&2, String>, ~(f => get_opt(~String, ~(s => s), f)),
4617    WValue{"str", ReadOpt{}}, m, id)
4618
4619def strs(m: Matches, id: String) -> Dec(List<&2, String>):
4620  read_with(~List<&2, String>, ~(f => get_many(~String, ~(s => s), f)),
4621    WValue{"str", ReadMany{}}, m, id)
4622
4623def nat(m: Matches, id: String) -> Dec(Nat):
4624  read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4625    WValue{"nat", ReadOne{}}, m, id)
4626
4627def nat_opt(m: Matches, id: String) -> Dec(Maybe<&2, Nat>):
4628  read_with(~Maybe<&2, Nat>, ~(f => get_opt(~Nat, ~nat_of, f)),
4629    WValue{"nat", ReadOpt{}}, m, id)
4630
4631def nats(m: Matches, id: String) -> Dec(List<&2, Nat>):
4632  read_with(~List<&2, Nat>, ~(f => get_many(~Nat, ~nat_of, f)),
4633    WValue{"nat", ReadMany{}}, m, id)
4634
4635def u32(m: Matches, id: String) -> Dec(U32):
4636  read_with(~U32, ~(f => get_one(~U32, ~u32_of, f)),
4637    WValue{"u32", ReadOne{}}, m, id)
4638
4639def u32_opt(m: Matches, id: String) -> Dec(Maybe<&2, U32>):
4640  read_with(~Maybe<&2, U32>, ~(f => get_opt(~U32, ~u32_of, f)),
4641    WValue{"u32", ReadOpt{}}, m, id)
4642
4643def u32s(m: Matches, id: String) -> Dec(List<&2, U32>):
4644  read_with(~List<&2, U32>, ~(f => get_many(~U32, ~u32_of, f)),
4645    WValue{"u32", ReadMany{}}, m, id)
4646
4647def f32(m: Matches, id: String) -> Dec(F32):
4648  read_with(~F32, ~(f => get_one(~F32, ~f32_of, f)),
4649    WValue{"f32", ReadOne{}}, m, id)
4650
4651def f32_opt(m: Matches, id: String) -> Dec(Maybe<&2, F32>):
4652  read_with(~Maybe<&2, F32>, ~(f => get_opt(~F32, ~f32_of, f)),
4653    WValue{"f32", ReadOpt{}}, m, id)
4654
4655def f32s(m: Matches, id: String) -> Dec(List<&2, F32>):
4656  read_with(~List<&2, F32>, ~(f => get_many(~F32, ~f32_of, f)),
4657    WValue{"f32", ReadMany{}}, m, id)
4658
4659def bool(m: Matches, id: String) -> Dec(Bool):
4660  read_with(~Bool, ~(f => get_one(~Bool, ~env_bool_true, f)),
4661    WValue{"bool", ReadOne{}}, m, id)
4662
4663def bool_opt(m: Matches, id: String) -> Dec(Maybe<&2, Bool>):
4664  read_with(~Maybe<&2, Bool>, ~(f => get_opt(~Bool, ~env_bool_true, f)),
4665    WValue{"bool", ReadOpt{}}, m, id)
4666
4667def bools(m: Matches, id: String) -> Dec(List<&2, Bool>):
4668  read_with(~List<&2, Bool>, ~(f => get_many(~Bool, ~env_bool_true, f)),
4669    WValue{"bool", ReadMany{}}, m, id)
4670
4671# A program and its arguments, as Cli.program reads them.
4672type Program is Data:
4673  Program{name: String, args: List<&2, String>}
4674
4675def program_of(values: List<&2, String>) -> Program:
4676  match values:
4677    case Nil{}: Program{"", []}
4678    case head <> tail: Program{head, tail}
4679
4680def program_maybe(values: List<&2, String>) -> Maybe<&2, Program>:
4681  match values:
4682    case Nil{}: None{}
4683    case head <> tail: Some{Program{head, tail}}
4684
4685def get_program(f: Field) -> Program:
4686  match f:
4687    case Field{id, values, source}: program_of(values)
4688
4689def get_program_opt(f: Field) -> Maybe<&2, Program>:
4690  match f:
4691    case Field{id, values, source}: program_maybe(values)
4692
4693# §6.5: a <COMMAND>... positional with Cli.rest() as a program and its
4694# arguments, the first value and the rest, as timeout, nice and env take
4695# them: every token after the program is one of its arguments.
4696def program(m: Matches, id: String) -> Dec(Program):
4697  read_with(~Program, ~get_program, WProgram{False{}}, m, id)
4698
4699def program_opt(m: Matches, id: String) -> Dec(Maybe<&2, Program>):
4700  read_with(~Maybe<&2, Program>, ~get_program_opt, WProgram{True{}}, m, id)
4701
4702def field_src(f: Field) -> Src:
4703  match f:
4704    case Field{id, values, source}: source
4705
4706def given(m: Matches, id: String) -> Dec(Bool):
4707  read_with(~Bool, ~(f => source_given(field_src(f))), WAny{}, m, id)
4708
4709def source(m: Matches, id: String) -> Dec(Src):
4710  read_with(~Src, ~field_src, WAny{}, m, id)
4711
4712def by_key_hit(a, -A: Kind(a), same: Bool, value: A) -> Maybe<a, A>:
4713  match same:
4714    case True{}: Some{value}
4715    case False{}: None{}
4716
4717# The value under the first key equal to key; after a hit it walks no further.
4718def find_by_key_go(a, -A: Kind(a), keys: List<&2, String>,
4719    values: List<a, A>, +key: String, found: Maybe<a, A>) -> Maybe<a, A>:
4720  match keys values found:
4721    case _ _ Some{hit}: Some{hit}
4722    case head <> tail value <> rest None{}:
4723      find_by_key_go(a, A, tail, rest, key,
4724        by_key_hit(a, A, same_text(head, key), value))
4725    case _ _ None{}: None{}
4726
4727def find_by_key(a, -A: Kind(a), keys: List<&2, String>, values: List<a, A>,
4728    +key: String) -> Maybe<a, A>:
4729  find_by_key_go(a, A, keys, values, key, None{})
4730
4731def keyed_cons(a, -A: Kind(a), key: String, value: A,
4732    rest: (List<&2, String> & List<a, A>)) -> (List<&2, String> & List<a, A>):
4733  (keys, values) = rest
4734  (key <> keys, value <> values)
4735
4736# A choose or route table as its keys and its values.
4737def unzip_keyed(a, -A: Kind(a), table: List<(String & A)>) ->
4738    (List<&2, String> & List<a, A>):
4739  match table:
4740    case Nil{}: ([], [])
4741    case entry <> tail:
4742      (key, value) = entry
4743      keyed_cons(a, A, key, value, unzip_keyed(a, A, tail))
4744
4745# After validation the key exists: parse and samples keep one_of values.
4746def choice_dec(-A: Data, found: Maybe<&2, A>) -> Dec(Maybe<&2, A>):
4747  match found:
4748    case Some{value}: Done{Some{value}}
4749    case None{}: Fail{Usage{"the value has no choose entry"}}
4750
4751def choose_read(-A: Data, keys: List<&2, String>, values: List<&2, A>,
4752    f: Field) -> Dec(Maybe<&2, A>):
4753  match f:
4754    case Field{id, head <> tail, source}:
4755      choice_dec(A, find_by_key(&2, A, keys, values, head))
4756    case _: Done{None{}}
4757
4758def choose_check(-A: Data, verdict: Maybe<&2, String>,
4759    keys: List<&2, String>, values: List<&2, A>,
4760    fields: List<&2, Field>, id: String) -> Dec(Maybe<&2, A>):
4761  match verdict:
4762    case None{}: choose_read(A, keys, values, field_find(fields, id))
4763    case Some{msg}: Fail{Bug{msg}}
4764
4765def choose_pick(-A: Data, opt: Bool,
4766    choices: (List<&2, String> & List<&2, A>), m: Matches,
4767    +id: String) -> Dec(Maybe<&2, A>):
4768  (+keys, values) = choices
4769  match m:
4770    case Matches{cmd, path, fields, external}:
4771      choose_check(A, verdict(cmd, path, id, WChoose{opt, keys}),
4772        keys, values, fields, id)
4773
4774def choose_opt(-A: Data, m: Matches, id: String,
4775    table: List<(String & A)>) -> Dec(Maybe<&2, A>):
4776  choose_pick(A, True{}, unzip_keyed(&2, A, table), m, id)
4777
4778# After validation a bare choose always has a value.
4779def choice_must(-A: Data, d: Dec(Maybe<&2, A>)) -> Dec(A):
4780  match d:
4781    case Fail{e}: Fail{e}
4782    case Done{Some{value}}: Done{value}
4783    case Done{None{}}: Fail{Usage{"the value is absent"}}
4784
4785def choose(-A: Data, m: Matches, id: String,
4786    table: List<(String & A)>) -> Dec(A):
4787  choice_must(A, choose_pick(A, False{}, unzip_keyed(&2, A, table), m, id))
4788
4789def Call(-A: Type) -> Type:
4790  @+m: Matches -> Dec(A)
4791
4792# Some{None{}} when text is the word name, Some{Some{rest}} when it is name,
4793# a space and rest, None{} otherwise. Words are cut at spaces, so a name with
4794# a space (Chr{32}) is no word.
4795def word_rest(name: String, text: String, same: Bool) ->
4796    Maybe<&2, Maybe<&2, String>>:
4797  match name text same:
4798    case _ _ False{}: None{}
4799    case SNil{} SNil{} True{}: Some{None{}}
4800    case SNil{} SCon{Chr{32}, rest} True{}: Some{Some{rest}}
4801    case SCon{Chr{32}, xs} _ True{}: None{}
4802    case SCon{x, xs} SCon{y, ys} True{}: word_rest(xs, ys, Char.is_eq(x, y))
4803    case _ _ _: None{}
4804
4805# How a route key's next word meets an item: not its subcommand, the
4806# subcommand of a built-in, the command the key ends at, or a command whose
4807# subcommands take the rest (§6.4).
4808type KeyStep is Data:
4809  KeyMiss{}
4810  KeyStop{}
4811  KeyEnd{}
4812  KeyNext{rest: String}
4813
4814def key_user(builtin: Bool, rest: Maybe<&2, String>) -> KeyStep:
4815  match builtin rest:
4816    case True{} _: KeyStop{}
4817    case False{} None{}: KeyEnd{}
4818    case False{} Some{text}: KeyNext{text}
4819
4820def key_word(found: Maybe<&2, Maybe<&2, String>>,
4821    children: List<&2, Item>) -> KeyStep:
4822  match found:
4823    case None{}: KeyMiss{}
4824    case Some{rest}: key_user(has_builtin(children), rest)
4825
4826def key_step(items: List<&2, Item>, text: String) -> KeyStep:
4827  match items:
4828    case Sub{name, about, children} <> tail:
4829      key_word(word_rest(name, text, True{}), children)
4830    case _: KeyMiss{}
4831
4832# The first subcommand named by each word of text, from items down, is a
4833# command.
4834def key_go(items: List<&2, Item>, +text: String, step: KeyStep) -> Bool:
4835  match items step:
4836    case Nil{} _: False{}
4837    case head <> +tail KeyMiss{}: key_go(tail, text, key_step(tail, text))
4838    case head <> tail KeyStop{}: False{}
4839    case head <> tail KeyEnd{}: True{}
4840    case Sub{name, about, +children} <> tail KeyNext{+rest}:
4841      key_go(children, rest, key_step(children, rest))
4842    case _ _: False{}
4843
4844def route_text(empty: Bool, +key: String, +items: List<&2, Item>) -> Bool:
4845  match empty:
4846    case True{}: True{}
4847    case False{}: key_go(items, key, key_step(items, key))
4848
4849# A route key's words name a command; "" is the root.
4850def route_key(+key: String, items: List<&2, Item>) -> Bool:
4851  route_text(String.is_empty(key), key, items)
4852
4853def non_command(keys: List<&2, String>, +items: List<&2, Item>) ->
4854    Maybe<&2, String>:
4855  match keys:
4856    case Nil{}: None{}
4857    case +key <> tail:
4858      Bool.pick(Maybe<&2, String>, route_key(key, items),
4859        non_command(tail, items), Some{key})
4860
4861def route_call(-A: Type, bad: Maybe<&2, String>, call: Call(A),
4862    m: Matches) -> Dec(A):
4863  match bad:
4864    case None{}: call(m)
4865    case Some{key}:
4866      Fail{Bug{String.concat(["route '", key, "' is not a command"])}}
4867
4868# No route for the path is checked first, then every key names a command.
4869def route_found(-A: Type, found: Maybe<Call(A)>, keys: List<&2, String>,
4870    cmd: Cmd, path: List<&2, String>, m: Matches) -> Dec(A):
4871  match found cmd:
4872    case None{} Cmd{name, about, items}:
4873      Fail{Bug{String.concat(["no route for '", path_text(name, path), "'"])}}
4874    case Some{call} Cmd{name, about, items}:
4875      route_call(A, non_command(keys, items), call, m)
4876
4877def route_pick(-A: Type, routes: (List<&2, String> & List<Call(A)>),
4878    m: Matches) -> Dec(A):
4879  (+keys, calls) = routes
4880  match m:
4881    case Matches{+cmd, +path, fields, external}:
4882      route_found(A,
4883        find_by_key(&1, Call(A), keys, calls, String.join(path, " ")),
4884        keys, cmd, path, Matches{cmd, path, fields, external})
4885
4886def route(-A: Type, m: Matches, table: List<(String & Call(A))>) -> Dec(A):
4887  route_pick(A, unzip_keyed(&1, Call(A), table), m)
4888
4889# ---- Samples and check (§6.6) ----
4890
4891def sample_typed(found: Maybe<&2, ValueType>) -> String:
4892  match found:
4893    case Some{TNat{}}: "0"
4894    case Some{TU32{}}: "0"
4895    case Some{TF32{}}: "0.0"
4896    case Some{TBool{}}: "false"
4897    case _: "x"
4898
4899def sample_choice(found: Maybe<&2, List<&2, String>>,
4900    attrs: List<&2, Item>) -> String:
4901  match found:
4902    case Some{head <> tail}: head
4903    case _: sample_typed(find_as(attrs))
4904
4905def sample_value(kind: UsageKind, +attrs: List<&2, Item>) -> String:
4906  match kind:
4907    case FlagKind{}: "true"
4908    case NegatableKind{}: "true"
4909    case CountKind{}: "1"
4910    case _: sample_choice(find_one_of(attrs), attrs)
4911
4912def sample_field(build: FieldBuild, info: ArgInfo) -> List<&2, Field>:
4913  match build:
4914    case Built{Nil{}}:
4915      match info:
4916        case ArgInfo{id, usage, short, long, kind, attrs}:
4917          [Field{id, [sample_value(kind, attrs)], Argv{}}]
4918    case Built{fields}: fields
4919    case BuildFault{fault}: Nil{}
4920
4921# Each arg in scope: its default as parse resolves it, else a sample value.
4922def sample_args(items: List<&2, Item>) -> List<&2, Field>:
4923  match items:
4924    case Nil{}: Nil{}
4925    case Arg{usage, help, attrs} <> tail:
4926      +info = arg_info(usage, attrs)
4927      List.append(&2, Field,
4928        sample_field(resolve_default(info_default(info), info), info),
4929        sample_args(tail))
4930    case head <> tail: sample_args(tail)
4931
4932def sample_fields(c: Cmd, path: List<&2, String>) -> List<&2, Field>:
4933  match c:
4934    case Cmd{name, about, items}: sample_args(scope_args(path, items, []))
4935
4936def sample(+c: Cmd, +path: List<&2, String>) -> Matches:
4937  Matches{c, path, sample_fields(c, path), None{}}
4938
4939# The paths below prefix that can complete, in pre-order, without built-ins.
4940def sub_paths(items: List<&2, Item>, +prefix: List<&2, String>) ->
4941    List<&2, List<&2, String>>:
4942  match items:
4943    case Nil{}: Nil{}
4944    case Sub{name, about, +children} <> tail:
4945      +path = List.append(&2, String, prefix, [name])
4946      List.concat(&2, List<&2, String>, [
4947        Bool.pick(List<&2, List<&2, String>>,
4948          Bool.or(has_builtin(children), needs_sub(children)), [], [path]),
4949        sub_paths(children, path),
4950        sub_paths(tail, prefix),
4951      ])
4952    case head <> tail: sub_paths(tail, prefix)
4953
4954def sample_paths(c: Cmd) -> List<&2, List<&2, String>>:
4955  match c:
4956    case Cmd{name, about, +items}:
4957      List.append(&2, List<&2, String>,
4958        Bool.pick(List<&2, List<&2, String>>, needs_sub(items), [], [[]]),
4959        sub_paths(items, []))
4960
4961def decode_bug(-A: Type, d: Dec(A), at: String) -> List<&2, Problem>:
4962  match d:
4963    case Fail{Bug{msg}}: [DecodeBug{at, msg}]
4964    case _: Nil{}
4965
4966def cmd_name(c: Cmd) -> String:
4967  match c:
4968    case Cmd{name, about, items}: name
4969
4970def sample_bugs(~A: Type, ~decode: @+m: Matches -> Dec(A),
4971    paths: List<&2, List<&2, String>>, +c: Cmd) -> List<&2, Problem>:
4972  match paths:
4973    case Nil{}: Nil{}
4974    case +path <> tail:
4975      List.append(&2, Problem,
4976        decode_bug(A, decode(sample(c, path)), path_text(cmd_name(c), path)),
4977        sample_bugs(~A, ~decode, tail, c))
4978
4979def check(~A: Type, ~decode: @+m: Matches -> Dec(A), +c: Cmd) ->
4980    List<&2, Problem>:
4981  List.append(&2, Problem, problems(c),
4982    sample_bugs(~A, ~decode, sample_paths(c), c))
4983# ---- Color (§10.6) ----
4984
4985type Term is Data:
4986  Term{out_color: Bool, err_color: Bool, width: Nat}
4987
4988def host_tty(h: Host, out: Bool, err: Bool) -> Host:
4989  match h:
4990    case Host{width, vars, out_tty, err_tty}: Host{width, vars, out, err}
4991
4992# Rules 1 and 2 apply only to color() exactly as declared.
4993def color_attrs(attrs: List<&2, Item>) -> Bool:
4994  match attrs:
4995    case Con{OneOf{values}, Con{AttrDefault{value}, Con{Global{}, Nil{}}}}:
4996      Bool.and(String.eq(String.join(values, " "), "auto always never"),
4997        String.eq(value, "auto"))
4998    case _: False{}
4999
5000def is_color(item: Item) -> Bool:
5001  match item:
5002    case Arg{usage, help, attrs}:
5003      Bool.and(String.eq(usage, "--color <WHEN>"),
5004        Bool.and(String.eq(help, "When to use color"), color_attrs(attrs)))
5005    case _: False{}
5006
5007def has_color(items: List<&2, Item>) -> Bool:
5008  match items:
5009    case Nil{}: False{}
5010    case item <> tail: Bool.or(is_color(item), has_color(tail))
5011
5012# The pre-scan of argv: the last --color value before `--`, "" for a bare
5013# --color before a hyphen token.
5014type Prescan is Data:
5015  ColorSeek{last: Maybe<&2, String>}
5016  ColorWant{}
5017  ColorStop{last: Maybe<&2, String>}
5018
5019def color_seek(dash: Bool, bare: Bool, attached: Bool, token: String,
5020    last: Maybe<&2, String>) -> Prescan:
5021  match dash bare attached:
5022    case True{} _ _: ColorStop{last}
5023    case False{} True{} _: ColorWant{}
5024    case False{} False{} True{}: ColorSeek{Some{String.drop(token, 8n)}}
5025    case False{} False{} False{}: ColorSeek{last}
5026
5027def color_want(hyphen: Bool, +token: String) -> Prescan:
5028  match hyphen:
5029    case False{}: ColorSeek{Some{token}}
5030    case True{}:
5031      color_seek(String.eq(token, "--"), String.eq(token, "--color"),
5032        String.starts_with(token, "--color="), token, Some{""})
5033
5034def color_step(state: Prescan, +token: String) -> Prescan:
5035  match state:
5036    case ColorSeek{last}:
5037      color_seek(String.eq(token, "--"), String.eq(token, "--color"),
5038        String.starts_with(token, "--color="), token, last)
5039    case ColorWant{}: color_want(String.starts_with(token, "-"), token)
5040    case ColorStop{last}: ColorStop{last}
5041
5042def color_scan(argv: List<&2, String>, state: Prescan) -> Prescan:
5043  match argv:
5044    case Nil{}: state
5045    case token <> tail: color_scan(tail, color_step(state, token))
5046
5047def color_word(always: Bool, never: Bool) -> Maybe<&2, Bool>:
5048  match always never:
5049    case True{} _: Some{True{}}
5050    case False{} True{}: Some{False{}}
5051    case False{} False{}: None{}
5052
5053def color_value(found: Maybe<&2, String>) -> Maybe<&2, Bool>:
5054  match found:
5055    case None{}: None{}
5056    case Some{+value}:
5057      color_word(String.eq(value, "always"), String.eq(value, "never"))
5058
5059def color_pick(state: Prescan) -> Maybe<&2, Bool>:
5060  match state:
5061    case ColorSeek{last}: color_value(last)
5062    case ColorWant{}: None{}
5063    case ColorStop{last}: color_value(last)
5064
5065def color_flag(enabled: Bool, argv: List<&2, String>) -> Maybe<&2, Bool>:
5066  match enabled:
5067    case True{}: color_pick(color_scan(argv, ColorSeek{None{}}))
5068    case False{}: None{}
5069
5070# An empty variable counts as unset.
5071def var_set(found: Maybe<&2, String>) -> Bool:
5072  match found:
5073    case None{}: False{}
5074    case Some{value}: Bool.not(String.is_empty(value))
5075
5076def var_is(found: Maybe<&2, String>, text: String) -> Bool:
5077  match found:
5078    case None{}: False{}
5079    case Some{value}: String.eq(value, text)
5080
5081def force_set(found: Maybe<&2, String>) -> Bool:
5082  match found:
5083    case None{}: False{}
5084    case Some{+value}:
5085      Bool.and(Bool.not(String.is_empty(value)),
5086        Bool.not(String.eq(value, "0")))
5087
5088def env_off(off: Bool) -> Maybe<&2, Bool>:
5089  match off:
5090    case True{}: Some{False{}}
5091    case False{}: None{}
5092
5093def env_force(force: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
5094  match force:
5095    case True{}: Some{True{}}
5096    case False{}:
5097      env_off(Bool.or(var_is(var_value(vars, "CLICOLOR"), "0"),
5098        var_is(var_value(vars, "TERM"), "dumb")))
5099
5100def env_rules(no_color: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
5101  match no_color:
5102    case True{}: Some{False{}}
5103    case False{}:
5104      env_force(force_set(var_value(vars, "CLICOLOR_FORCE")), vars)
5105
5106# Rules 3-6; None leaves rule 7, each stream's TTY.
5107def env_color(+vars: List<&2, Var>) -> Maybe<&2, Bool>:
5108  env_rules(var_set(var_value(vars, "NO_COLOR")), vars)
5109
5110def term_env(found: Maybe<&2, Bool>, out_tty: Bool, err_tty: Bool,
5111    width: Nat) -> Term:
5112  match found:
5113    case Some{+on}: Term{on, on, width}
5114    case None{}: Term{out_tty, err_tty, width}
5115
5116def term_for(flag: Maybe<&2, Bool>, vars: List<&2, Var>, out_tty: Bool,
5117    err_tty: Bool, width: Nat) -> Term:
5118  match flag:
5119    case Some{+on}: Term{on, on, width}
5120    case None{}: term_env(env_color(vars), out_tty, err_tty, width)
5121
5122def term(c: Cmd, h: Host, argv: List<&2, String>) -> Term:
5123  match c h:
5124    case Cmd{name, about, items} Host{width, vars, out_tty, err_tty}:
5125      term_for(color_flag(has_color(items), argv), vars, out_tty, err_tty,
5126        width)
5127
5128# ---- Help (§10.2, §10.3) ----
5129
5130def plain(text: String) -> Segment:
5131  Segment{text, []}
5132
5133def page_width(width: Nat) -> Nat:
5134  Nat.min(100n, Nat.max(40n, width))
5135
5136# The first paragraph: the text before the first blank line.
5137def summary(s: String) -> String:
5138  match s:
5139    case SNil{}: SNil{}
5140    case SCon{Chr{10}, SCon{Chr{10}, tail}}: SNil{}
5141    case SCon{head, tail}: SCon{head, summary(tail)}
5142
5143def shown_text(long: Bool, text: String) -> String:
5144  match long:
5145    case True{}: text
5146    case False{}: summary(text)
5147
5148def filled(s: String) -> Bool:
5149  Bool.not(String.is_empty(s))
5150
5151def join_filled(parts: List<&2, String>) -> String:
5152  String.join(List.filter(~String, ~filled, parts), " ")
5153
5154# The command at the longest valid prefix of a path, with the globals it
5155# inherits, root first; stuck once a step names no subcommand.
5156type View is Data:
5157  View{path: String, about: String, items: List<&2, Item>,
5158    globals: List<&2, Item>, root: Bool, stuck: Bool}
5159
5160def find_command(items: List<&2, Item>, +query: String,
5161    found: Maybe<&2, Item>) -> Maybe<&2, Item>:
5162  match items found:
5163    case _ Some{item}: Some{item}
5164    case Nil{} None{}: None{}
5165    case Sub{+name, about, +children} <> tail None{}:
5166      find_command(tail, query, Bool.pick(Maybe<&2, Item>,
5167        Bool.or(String.eq(query, name),
5168          List.contains(~String, ~String.eq, find_aliases(children), query)),
5169        Some{Sub{name, about, children}}, None{}))
5170    case head <> tail None{}: find_command(tail, query, None{})
5171
5172def view_enter(found: Maybe<&2, Item>, path: String, about: String,
5173    items: List<&2, Item>, globals: List<&2, Item>, root: Bool) -> View:
5174  match found:
5175    case Some{Sub{name, sub_about, children}}:
5176      View{String.concat([path, " ", name]), sub_about, children,
5177        List.append(&2, Item, globals, global_items(items)), False{}, False{}}
5178    case _: View{path, about, items, globals, root, True{}}
5179
5180def view_step(view: View, step: String) -> View:
5181  match view:
5182    case View{path, about, items, globals, root, True{}}:
5183      View{path, about, items, globals, root, True{}}
5184    case View{path, about, +items, globals, root, False{}}:
5185      view_enter(find_command(items, step, None{}), path, about, items,
5186        globals, root)
5187
5188def view_walk(steps: List<&2, String>, view: View) -> View:
5189  match steps:
5190    case Nil{}: view
5191    case step <> tail: view_walk(tail, view_step(view, step))
5192
5193def view_of(c: Cmd, path: List<&2, String>) -> View:
5194  match c:
5195    case Cmd{name, about, items}:
5196      view_walk(path, View{name, about, items, [], True{}, False{}})
5197
5198def arg_pos(usage: String) -> Bool:
5199  usage_kind(~kind_pos, parse_usage(usage))
5200
5201def has_plain_option(items: List<&2, Item>) -> Bool:
5202  match items:
5203    case Nil{}: False{}
5204    case Arg{usage, help, +attrs} <> tail:
5205      Bool.or(Bool.and(Bool.not(arg_pos(usage)),
5206        Bool.and(Bool.not(has_hidden(attrs)), Bool.not(has_required(attrs)))),
5207        has_plain_option(tail))
5208    case head <> tail: has_plain_option(tail)
5209
5210def required_elem(required: Bool, usage: String, attrs: List<&2, Item>) ->
5211    List<&2, Segment>:
5212  match required:
5213    case True{}: [plain(arg_display(arg_info(usage, attrs)))]
5214    case False{}: []
5215
5216def required_options(items: List<&2, Item>) -> List<&2, Segment>:
5217  match items:
5218    case Nil{}: Nil{}
5219    case Arg{+usage, help, +attrs} <> tail:
5220      List.append(&2, Segment,
5221        required_elem(Bool.and(Bool.not(arg_pos(usage)), has_required(attrs)),
5222          usage, attrs),
5223        required_options(tail))
5224    case head <> tail: required_options(tail)
5225
5226def group_elem(listed: Bool, args: List<&2, Item>, group: String) ->
5227    List<&2, Segment>:
5228  match listed:
5229    case True{}: []
5230    case False{}: [plain(group_display(args, group))]
5231
5232# Each need_one group of items, over args, that done does not name yet.
5233def group_elems(items: List<&2, Item>, +args: List<&2, Item>,
5234    +done: List<&2, String>) -> List<&2, Segment>:
5235  match items:
5236    case Nil{}: Nil{}
5237    case NeedOne{+group} <> tail:
5238      List.append(&2, Segment,
5239        group_elem(List.contains(~String, ~String.eq, done, group), args,
5240          group),
5241        group_elems(tail, args, group <> done))
5242    case head <> tail: group_elems(tail, args, done)
5243
5244def positional_elems(items: List<&2, Item>) -> List<&2, Segment>:
5245  match items:
5246    case Nil{}: Nil{}
5247    case Arg{+usage, help, attrs} <> tail:
5248      +rest = positional_elems(tail)
5249      Bool.pick(List<&2, Segment>, arg_pos(usage), plain(usage) <> rest, rest)
5250    case head <> tail: positional_elems(tail)
5251
5252def command_elems(subs: Bool, need: Bool) -> List<&2, Segment>:
5253  match subs need:
5254    case False{} _: []
5255    case True{} True{}: [plain("<COMMAND>")]
5256    case True{} False{}: [plain("[COMMAND]")]
5257
5258def options_elem(present: Bool) -> List<&2, Segment>:
5259  match present:
5260    case True{}: [plain("[OPTIONS]")]
5261    case False{}: []
5262
5263# The path, [OPTIONS], required options, need_one groups as E06 writes them,
5264# positionals, then the subcommand slot.
5265def usage_elems(path: String, +items: List<&2, Item>,
5266    +globals: List<&2, Item>) -> List<&2, Segment>:
5267  +args = List.append(&2, Item, items, globals)
5268  +scope = List.append(&2, Item, globals, items)
5269  List.concat(&2, Segment, [
5270    [Segment{path, [Bold{}]}],
5271    options_elem(has_plain_option(args)),
5272    required_options(args),
5273    group_elems(scope, scope, []),
5274    positional_elems(items),
5275    command_elems(has_sub_item(items), needs_sub(items)),
5276  ])
5277
5278def segment_texts(segs: List<&2, Segment>) -> List<&2, String>:
5279  match segs:
5280    case Nil{}: Nil{}
5281    case Segment{text, styles} <> tail: text <> segment_texts(tail)
5282
5283def usage_view(view: View) -> String:
5284  match view:
5285    case View{path, about, items, globals, root, stuck}:
5286      String.join(segment_texts(usage_elems(path, items, globals)), " ")
5287
5288def usage(c: Cmd, path: List<&2, String>) -> String:
5289  usage_view(view_of(c, path))
5290
5291# Usage wraps between elements and continues at column 7.
5292def usage_rest(elems: List<&2, Segment>, +w: Nat, +col: Nat) ->
5293    List<&2, Segment>:
5294  match elems:
5295    case Nil{}: Nil{}
5296    case Segment{+text, styles} <> tail:
5297      +ew = width(text)
5298      +fits = Nat.is_le((col + 1n + ew : Nat), w)
5299      plain(Bool.pick(String, fits, " ", "\n       ")) <>
5300        Segment{text, styles} <> usage_rest(tail, w,
5301          Bool.pick(Nat, fits, (col + 1n + ew : Nat), (7n + ew : Nat)))
5302
5303def usage_block(elems: List<&2, Segment>, w: Nat) -> List<&2, Segment>:
5304  match elems:
5305    case Nil{}: [Segment{"Usage:", [Bold{}, Under{}]}]
5306    case Segment{+text, styles} <> tail:
5307      Segment{"Usage:", [Bold{}, Under{}]} <> plain(" ") <>
5308        Segment{text, styles} <> usage_rest(tail, w, (7n + width(text) : Nat))
5309
5310# A help row: its left cell, whether it is an option with a short or a
5311# long-only option, and its right text.
5312type Row is Data:
5313  Row{left: List<&2, Segment>, short: Bool, long_only: Bool, right: String}
5314
5315type Section is Data:
5316  Section{title: String, rows: List<&2, Row>}
5317
5318def tag(name: String, values: List<&2, String>) -> String:
5319  match values:
5320    case Nil{}: ""
5321    case head <> tail:
5322      String.concat(["[", name, ": ", String.join(head <> tail, ", "), "]"])
5323
5324def maybe_list(found: Maybe<&2, String>) -> List<&2, String>:
5325  match found:
5326    case None{}: Nil{}
5327    case Some{value}: [value]
5328
5329def list_or_none(found: Maybe<&2, List<&2, String>>) -> List<&2, String>:
5330  match found:
5331    case None{}: Nil{}
5332    case Some{values}: values
5333
5334def right_text(long: Bool, help: String, +attrs: List<&2, Item>) -> String:
5335  join_filled([
5336    shown_text(long, help),
5337    tag("env", find_envs(attrs)),
5338    tag("default", maybe_list(find_default(attrs))),
5339    tag("possible values", list_or_none(find_one_of(attrs))),
5340    tag("aliases", find_aliases(attrs)),
5341  ])
5342
5343def bold_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5344  match names:
5345    case Nil{}: Nil{}
5346    case name <> tail:
5347      List.append(&2, Segment,
5348        Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5349        Segment{String.trim_start(name), [Bold{}]} <> bold_names(tail, False{}))
5350
5351# Where an option's value or repeat starts: " <", "[=", or a final "...".
5352def value_cut(s: String) -> Nat:
5353  match s:
5354    case SNil{}: 0n
5355    case SCon{Chr{32}, SCon{Chr{60}, tail}}: 0n
5356    case SCon{Chr{91}, SCon{Chr{61}, tail}}: 0n
5357    case SCon{Chr{46}, SCon{Chr{46}, SCon{Chr{46}, SNil{}}}}: 0n
5358    case SCon{head, tail}: 1n+value_cut(tail)
5359
5360# Option names bold; separators, value and repeat plain.
5361def option_left(+usage: String) -> List<&2, Segment>:
5362  +cut = value_cut(usage)
5363  List.append(&2, Segment,
5364    bold_names(String.split(String.take(usage, cut), ','), True{}),
5365    [plain(String.drop(usage, cut))])
5366
5367def arg_row(parsed: Usage, usage: String, help: String,
5368    attrs: List<&2, Item>, long: Bool) -> Row:
5369  match parsed:
5370    case Parsed{id, +short, name, kind}:
5371      Row{option_left(usage), filled(short), String.is_empty(short),
5372        right_text(long, help, attrs)}
5373    case Invalid{col}:
5374      Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}
5375
5376def find_heading(attrs: List<&2, Item>) -> String:
5377  match attrs:
5378    case Nil{}: ""
5379    case Heading{title} <> tail: title
5380    case head <> tail: find_heading(tail)
5381
5382def option_row(shown: Bool, +usage: String, help: String,
5383    attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5384  match shown:
5385    case True{}: [arg_row(parse_usage(usage), usage, help, attrs, long)]
5386    case False{}: []
5387
5388# The visible options under a heading, "" for Options.
5389def option_rows(items: List<&2, Item>, +long: Bool, +heading: String) ->
5390    List<&2, Row>:
5391  match items:
5392    case Nil{}: Nil{}
5393    case Arg{+usage, help, +attrs} <> tail:
5394      List.append(&2, Row,
5395        option_row(Bool.and(Bool.not(arg_pos(usage)),
5396          Bool.and(Bool.not(has_hidden(attrs)),
5397            String.eq(find_heading(attrs), heading))),
5398          usage, help, attrs, long),
5399        option_rows(tail, long, heading))
5400    case head <> tail: option_rows(tail, long, heading)
5401
5402def positional_row(shown: Bool, usage: String, help: String,
5403    attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5404  match shown:
5405    case True{}:
5406      [Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}]
5407    case False{}: []
5408
5409def positional_rows(items: List<&2, Item>, +long: Bool) -> List<&2, Row>:
5410  match items:
5411    case Nil{}: Nil{}
5412    case Arg{+usage, help, +attrs} <> tail:
5413      List.append(&2, Row,
5414        positional_row(Bool.and(arg_pos(usage), Bool.not(has_hidden(attrs))),
5415          usage, help, attrs, long),
5416        positional_rows(tail, long))
5417    case head <> tail: positional_rows(tail, long)
5418
5419def command_row(hidden: Bool, name: String, about: String,
5420    children: List<&2, Item>) -> List<&2, Row>:
5421  match hidden:
5422    case True{}: []
5423    case False{}:
5424      [Row{[Segment{name, [Bold{}]}], False{}, False{},
5425        join_filled([summary(about), tag("aliases", find_aliases(children))])}]
5426
5427def command_rows(items: List<&2, Item>) -> List<&2, Row>:
5428  match items:
5429    case Nil{}: Nil{}
5430    case Sub{name, about, +children} <> tail:
5431      List.append(&2, Row,
5432        command_row(has_hidden(children), name, about, children),
5433        command_rows(tail))
5434    case head <> tail: command_rows(tail)
5435
5436def help_sub_row(present: Bool) -> List<&2, Row>:
5437  match present:
5438    case True{}:
5439      [Row{[Segment{"help", [Bold{}]}], False{}, False{},
5440        "Print this message or the help of the given subcommand(s)"}]
5441    case False{}: []
5442
5443# A built-in option row, without its short when an app arg took it.
5444def builtin_row(taken: Bool, short: String, long: String,
5445    text: String) -> Row:
5446  match taken:
5447    case True{}: Row{bold_names([long], True{}), False{}, True{}, text}
5448    case False{}:
5449      Row{bold_names([short, long], True{}), True{}, False{}, text}
5450
5451def help_text(differs: Bool, long: Bool) -> String:
5452  match differs long:
5453    case False{} _: "Print help"
5454    case True{} False{}: "Print help (see more with '--help')"
5455    case True{} True{}: "Print help (see a summary with '-h')"
5456
5457def version_row(present: Bool, args: List<&2, Item>) -> List<&2, Row>:
5458  match present:
5459    case True{}:
5460      [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-V")),
5461        "-V", "--version", "Print version")]
5462    case False{}: []
5463
5464# Headings of visible args, in order of first use.
5465def heading_titles(items: List<&2, Item>, +seen: List<&2, String>) ->
5466    List<&2, String>:
5467  match items:
5468    case Nil{}: seen
5469    case Arg{usage, help, +attrs} <> tail:
5470      +title = find_heading(attrs)
5471      heading_titles(tail, Bool.pick(List<&2, String>,
5472        Bool.or(String.is_empty(title), Bool.or(has_hidden(attrs),
5473          List.contains(~String, ~String.eq, seen, title))),
5474        seen, List.append(&2, String, seen, [title])))
5475    case head <> tail: heading_titles(tail, seen)
5476
5477def heading_sections(titles: List<&2, String>, +args: List<&2, Item>,
5478    +long: Bool) -> List<&2, Section>:
5479  match titles:
5480    case Nil{}: Nil{}
5481    case +title <> tail:
5482      Section{title, option_rows(args, long, title)} <>
5483        heading_sections(tail, args, long)
5484
5485# Whether a visible arg's help has more than one paragraph.
5486def args_paragraphs(items: List<&2, Item>) -> Bool:
5487  match items:
5488    case Nil{}: False{}
5489    case Arg{usage, help, attrs} <> tail:
5490      Bool.or(Bool.and(Bool.not(has_hidden(attrs)),
5491        String.contains(help, "\n\n")), args_paragraphs(tail))
5492    case head <> tail: args_paragraphs(tail)
5493
5494def indent_cell(indent: Bool, left: List<&2, Segment>) -> List<&2, Segment>:
5495  match indent:
5496    case True{}: plain("    ") <> left
5497    case False{}: left
5498
5499# A left cell; long-only options indent 4 more on a page with a short.
5500def row_cell(row: Row, shorted: Bool) -> List<&2, Segment>:
5501  match row:
5502    case Row{left, short, long_only, right}:
5503      indent_cell(Bool.and(long_only, shorted), left)
5504
5505def cell_width(cell: List<&2, Segment>) -> Nat:
5506  match cell:
5507    case Nil{}: 0n
5508    case Segment{text, styles} <> tail: Nat.add(width(text), cell_width(tail))
5509
5510def row_short(row: Row) -> Bool:
5511  match row:
5512    case Row{left, short, long_only, right}: short
5513
5514def any_short(rows: List<&2, Row>) -> Bool:
5515  match rows:
5516    case Nil{}: False{}
5517    case row <> tail: Bool.or(row_short(row), any_short(tail))
5518
5519# L, the widest left cell w with 2 + w + 2 <= K.
5520def widest(rows: List<&2, Row>, +shorted: Bool, +k: Nat, +best: Nat) -> Nat:
5521  match rows:
5522    case Nil{}: best
5523    case row <> tail:
5524      +w = cell_width(row_cell(row, shorted))
5525      widest(tail, shorted, k,
5526        Bool.pick(Nat, Nat.is_le((w + 4n : Nat), k), Nat.max(w, best), best))
5527
5528def pad_line(line: String, col: Nat) -> String:
5529  match line:
5530    case SNil{}: "\n"
5531    case SCon{head, tail}:
5532      String.concat(["\n", String.repeat(" ", col), SCon{head, tail}])
5533
5534def pad_lines(lines: List<&2, String>, +col: Nat) -> List<&2, Segment>:
5535  match lines:
5536    case Nil{}: Nil{}
5537    case line <> tail: plain(pad_line(line, col)) <> pad_lines(tail, col)
5538
5539def first_then(lines: List<&2, String>, col: Nat) -> List<&2, Segment>:
5540  match lines:
5541    case Nil{}: Nil{}
5542    case line <> tail: plain(line) <> pad_lines(tail, col)
5543
5544# A cell that fits takes its text after padding to C; one that does not
5545# stands alone, its text on the next lines at max(C, 10).
5546def row_form(fits: Bool, cell: List<&2, Segment>, lw: Nat, right: String,
5547    w: Nat, +c: Nat) -> List<&2, Segment>:
5548  match fits:
5549    case True{}:
5550      plain("  ") <> List.append(&2, Segment, cell,
5551        plain(String.repeat(" ", (c - 2n - lw : Nat))) <>
5552          first_then(String.lines(wrap(right, (w - c : Nat))), c))
5553    case False{}:
5554      +col = Nat.max(c, 10n)
5555      plain("  ") <> List.append(&2, Segment, cell,
5556        pad_lines(String.lines(wrap(right, (w - col : Nat))), col))
5557
5558def row_lines(row: Row, shorted: Bool, w: Nat, k: Nat, c: Nat) ->
5559    List<&2, Segment>:
5560  match row:
5561    case Row{left, short, long_only, right}:
5562      +cell = indent_cell(Bool.and(long_only, shorted), left)
5563      +lw = cell_width(cell)
5564      row_form(Nat.is_le((lw + 4n : Nat), k), cell, lw, right, w, c)
5565
5566def rows_lines(rows: List<&2, Row>, +shorted: Bool, +w: Nat, +k: Nat,
5567    +c: Nat) -> List<&2, Segment>:
5568  match rows:
5569    case Nil{}: Nil{}
5570    case row <> tail:
5571      plain("\n") <> List.append(&2, Segment, row_lines(row, shorted, w, k, c),
5572        rows_lines(tail, shorted, w, k, c))
5573
5574def section_rows(sections: List<&2, Section>) -> List<&2, Row>:
5575  match sections:
5576    case Nil{}: Nil{}
5577    case Section{title, rows} <> tail:
5578      List.append(&2, Row, rows, section_rows(tail))
5579
5580def section_blocks(sections: List<&2, Section>, +shorted: Bool, +w: Nat,
5581    +k: Nat, +c: Nat) -> List<&2, List<&2, Segment>>:
5582  match sections:
5583    case Nil{}: Nil{}
5584    case Section{title, Nil{}} <> tail: section_blocks(tail, shorted, w, k, c)
5585    case Section{title, rows} <> tail:
5586      (Segment{String.append(title, ":"), [Bold{}, Under{}]} <>
5587        rows_lines(rows, shorted, w, k, c)) <>
5588        section_blocks(tail, shorted, w, k, c)
5589
5590# One column for the page: C = L + 4, with K = 2W/5.
5591def sections_blocks(+sections: List<&2, Section>, +w: Nat) ->
5592    List<&2, List<&2, Segment>>:
5593  +rows = section_rows(sections)
5594  +shorted = any_short(rows)
5595  +k = Nat.div((2n * w : Nat), 5n)
5596  section_blocks(sections, shorted, w, k,
5597    (widest(rows, shorted, k, 0n) + 4n : Nat))
5598
5599def text_block(text: String, w: Nat) -> List<&2, List<&2, Segment>>:
5600  match text:
5601    case SNil{}: Nil{}
5602    case SCon{head, tail}: [[plain(wrap(SCon{head, tail}, w))]]
5603
5604def example_lines(items: List<&2, Item>) -> List<&2, Segment>:
5605  match items:
5606    case Nil{}: Nil{}
5607    case Example{line, what} <> tail:
5608      plain(String.concat(["\n  ", what, ":\n    $ ", line])) <>
5609        example_lines(tail)
5610    case head <> tail: example_lines(tail)
5611
5612def examples_block(lines: List<&2, Segment>) -> List<&2, List<&2, Segment>>:
5613  match lines:
5614    case Nil{}: Nil{}
5615    case head <> tail: [Segment{"Examples:", [Bold{}, Under{}]} <> head <> tail]
5616
5617def after_texts(items: List<&2, Item>) -> List<&2, String>:
5618  match items:
5619    case Nil{}: Nil{}
5620    case After{text} <> tail: text <> after_texts(tail)
5621    case head <> tail: after_texts(tail)
5622
5623def more_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5624  match blocks:
5625    case Nil{}: [plain("\n")]
5626    case block <> tail:
5627      plain("\n\n") <> List.append(&2, Segment, block, more_blocks(tail))
5628
5629# Blocks apart by one blank line; the page ends with a newline.
5630def join_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5631  match blocks:
5632    case Nil{}: Nil{}
5633    case block <> tail: List.append(&2, Segment, block, more_blocks(tail))
5634
5635def help_page(view: View, +long: Bool, +w: Nat) -> List<&2, Segment>:
5636  match view:
5637    case View{path, +about, +items, +globals, root, stuck}:
5638      +args = List.append(&2, Item, items, globals)
5639      join_blocks(List.concat(&2, List<&2, Segment>, [
5640        text_block(shown_text(long, about), w),
5641        [usage_block(usage_elems(path, items, globals), w)],
5642        sections_blocks(List.append(&2, Section, [
5643          Section{"Commands", List.append(&2, Row, command_rows(items),
5644            help_sub_row(has_sub_item(items)))},
5645          Section{"Arguments", positional_rows(items, long)},
5646          Section{"Options", List.concat(&2, Row, [
5647            option_rows(args, long, ""),
5648            [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-h")),
5649              "-h", "--help", help_text(Bool.or(String.contains(about, "\n\n"),
5650                args_paragraphs(args)), long))],
5651            version_row(Bool.and(root, has_version_item(items)), args),
5652          ])},
5653        ], heading_sections(heading_titles(args, []), args, long)), w),
5654        examples_block(example_lines(items)),
5655        text_block(String.join(after_texts(items), "\n\n"), w),
5656      ]))
5657
5658def help_out(t: Term, view: View, long: Bool) -> String:
5659  match t:
5660    case Term{out_color, err_color, width}:
5661      emit(out_color, help_page(view, long, page_width(width)))
5662
5663# The help page for the longest valid prefix of path, in stdout's colors.
5664def help(+c: Cmd, path: List<&2, String>, long: Bool, h: Host) -> String:
5665  help_out(term(c, h, []), view_of(c, path), long)
5666
5667# ---- Suggestions (§10.4) ----
5668
5669type Scored is Data:
5670  Scored{dist: Nat, name: String}
5671
5672def row_zero(b: String, +k: Nat) -> List<&2, Nat>:
5673  match b:
5674    case SNil{}: [k]
5675    case SCon{head, tail}: k <> row_zero(tail, (k + 1n : Nat))
5676
5677def head_nat(xs: List<&2, Nat>) -> Nat:
5678  match xs:
5679    case Nil{}: 0n
5680    case head <> tail: head
5681
5682def tail_nats(xs: List<&2, Nat>) -> List<&2, Nat>:
5683  match xs:
5684    case Nil{}: Nil{}
5685    case head <> tail: tail
5686
5687def same_char(found: Maybe<&2, Char>, c: Char) -> Bool:
5688  match found:
5689    case None{}: False{}
5690    case Some{d}: Char.is_eq(d, c)
5691
5692# Row i of optimal string alignment for a[i]: up starts at d[i-1][j-1], up2
5693# at d[i-2][j-2], and left is d[i][j-1].
5694def osa_row(bs: String, bp: Maybe<&2, Char>, +a: Char,
5695    +ap: Maybe<&2, Char>, +up: List<&2, Nat>, +up2: List<&2, Nat>,
5696    left: Nat) -> List<&2, Nat>:
5697  match bs:
5698    case SNil{}: Nil{}
5699    case SCon{+b, tail}:
5700      +diag = head_nat(up)
5701      +rest = tail_nats(up)
5702      +edit = Nat.min(Nat.min((head_nat(rest) + 1n : Nat), (left + 1n : Nat)),
5703        Bool.pick(Nat, Char.is_eq(a, b), diag, (diag + 1n : Nat)))
5704      +best = Bool.pick(Nat, Bool.and(same_char(bp, a), same_char(ap, b)),
5705        Nat.min(edit, (head_nat(up2) + 1n : Nat)), edit)
5706      best <> osa_row(tail, Some{b}, a, ap, rest, tail_nats(up2), best)
5707
5708def osa_rows(s: String, ap: Maybe<&2, Char>, +i: Nat, +b: String,
5709    +prev: List<&2, Nat>, prev2: List<&2, Nat>) -> List<&2, Nat>:
5710  match s:
5711    case SNil{}: prev
5712    case SCon{+a, tail}:
5713      osa_rows(tail, Some{a}, (i + 1n : Nat), b,
5714        i <> osa_row(b, None{}, a, ap, prev, 0n <> prev2, i), prev)
5715
5716def last_nat(xs: List<&2, Nat>, last: Nat) -> Nat:
5717  match xs:
5718    case Nil{}: last
5719    case head <> tail: last_nat(tail, head)
5720
5721def osa(a: String, +b: String) -> Nat:
5722  +first = row_zero(b, 0n)
5723  last_nat(osa_rows(a, None{}, 1n, b, first, first), 0n)
5724
5725def undashed(s: String) -> String:
5726  match s:
5727    case SCon{Chr{45}, tail}: undashed(tail)
5728    case _: s
5729
5730def scored(names: List<&2, String>, +typed: String, +limit: Nat) ->
5731    List<&2, Scored>:
5732  match names:
5733    case Nil{}: Nil{}
5734    case +name <> tail:
5735      +dist = osa(typed, String.take(name, 64n))
5736      +rest = scored(tail, typed, limit)
5737      Bool.pick(List<&2, Scored>, Nat.is_le(dist, limit),
5738        Scored{dist, name} <> rest, rest)
5739
5740def at_distance(found: List<&2, Scored>, +d: Nat) -> List<&2, String>:
5741  match found:
5742    case Nil{}: Nil{}
5743    case Scored{dist, name} <> tail:
5744      +rest = at_distance(tail, d)
5745      Bool.pick(List<&2, String>, Nat.is_eq(dist, d), name <> rest, rest)
5746
5747def by_distance(levels: Nat, +d: Nat, +found: List<&2, Scored>) ->
5748    List<&2, String>:
5749  match levels:
5750    case 0n: Nil{}
5751    case 1n+more:
5752      List.append(&2, String, at_distance(found, d),
5753        by_distance(more, (d + 1n : Nat), found))
5754
5755# At most 3 names within max(1, n / 3) edits, by distance, then order.
5756# key is bound because a temporary argument makes String.length take its
5757# string, adding a keep to each caller that reuses one (Nat.read.fits,
5758# arg_display).
5759def suggest(+typed: String, names: List<&2, String>) -> List<&2, String>:
5760  key = undashed(typed)
5761  +limit = Nat.max(1n, Nat.div(String.length(key), 3n))
5762  List.take(&2, String, by_distance((limit + 1n : Nat), 0n,
5763    scored(names, String.take(typed, 64n), limit)), 3n)
5764
5765# ---- Errors (§10.4) ----
5766
5767def quoted(text: String, styles: List<&2, Style>) -> List<&2, Segment>:
5768  [plain("'"), Segment{text, styles}, plain("'")]
5769
5770def missing_lines(missing: List<&2, String>) -> String:
5771  match missing:
5772    case Nil{}: ""
5773    case head <> tail: String.concat(["\n  ", head, missing_lines(tail)])
5774
5775def visible_subs(items: List<&2, Item>) -> List<&2, String>:
5776  match items:
5777    case Nil{}: ["help"]
5778    case Sub{name, about, children} <> tail:
5779      +rest = visible_subs(tail)
5780      Bool.pick(List<&2, String>, has_hidden(children), rest, name <> rest)
5781    case head <> tail: visible_subs(tail)
5782
5783def fault_message(fault: ScanFault, path: String,
5784    items: List<&2, Item>) -> List<&2, Segment>:
5785  match fault:
5786    case E01{token}:
5787      List.concat(&2, Segment, [[plain("unexpected argument ")],
5788        quoted(token, [Yellow{}]), [plain(" found")]])
5789    case E02{name}:
5790      plain("unrecognized subcommand ") <> quoted(name, [Yellow{}])
5791    case E03{arg}:
5792      [plain(String.concat(["a value is required for '", arg,
5793        "' but none was supplied"]))]
5794    case E04{arg, value, reason}:
5795      List.concat(&2, Segment, [[plain("invalid value ")],
5796        quoted(value, [Yellow{}]),
5797        [plain(String.concat([" for '", arg, "': ", reason]))]])
5798    case E05{arg, value}:
5799      List.concat(&2, Segment, [[plain("unexpected value ")],
5800        quoted(value, [Yellow{}]),
5801        [plain(String.concat([" for '", arg,
5802          "' found; no more were expected"]))]])
5803    case E06{missing}:
5804      [plain(String.append(
5805        "the following required arguments were not provided:",
5806        missing_lines(missing)))]
5807    case E07{a, b}:
5808      [plain(String.concat(["the argument '", a, "' cannot be used with '", b,
5809        "'"]))]
5810    case E08{a, b}:
5811      [plain(String.concat(["the argument '", a, "' requires '", b, "'"]))]
5812    case E09{token}:
5813      List.concat(&2, Segment, [[plain("unexpected argument ")],
5814        quoted(token, [Yellow{}]), [plain(" found")]])
5815    case E10{}:
5816      [plain(String.concat(["'", path,
5817        "' requires a subcommand but none was provided\n  [subcommands: ",
5818        String.join(visible_subs(items), ", "), "]"]))]
5819    case E11{arg, value, name, reason}:
5820      List.concat(&2, Segment, [[plain("invalid value ")],
5821        quoted(value, [Yellow{}]),
5822        [plain(String.concat([" for '", arg, "' from environment variable ",
5823          name, ": ", reason]))]])
5824
5825def green_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5826  match names:
5827    case Nil{}: Nil{}
5828    case name <> tail:
5829      List.concat(&2, Segment, [
5830        Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5831        quoted(name, [Green{}]), green_names(tail, False{})])
5832
5833def tip(text: String) -> List<&2, List<&2, Segment>>:
5834  [[plain(text)]]
5835
5836# T4: one suggestion or several.
5837def similar(kind: String, found: List<&2, String>) ->
5838    List<&2, List<&2, Segment>>:
5839  match found:
5840    case Nil{}: Nil{}
5841    case name <> Nil{}:
5842      [plain(String.concat(["a similar ", kind, " exists: "])) <>
5843        quoted(name, [Green{}])]
5844    case name <> more:
5845      [plain(String.concat(["some similar ", kind, "s exist: "])) <>
5846        green_names(name <> more, True{})]
5847
5848def long_name(s: String) -> Bool:
5849  String.starts_with(s, "--")
5850
5851def option_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
5852  match parsed:
5853    case Parsed{id, short, +long, kind}:
5854      List.concat(&2, String, [
5855        Bool.pick(List<&2, String>, String.is_empty(long), [], [long]),
5856        Bool.pick(List<&2, String>,
5857          Bool.and(kind_negatable(kind), filled(long)),
5858          [negated_long(long)], []),
5859        List.filter(~String, ~long_name, find_aliases(attrs)),
5860      ])
5861    case Invalid{col}: []
5862
5863def visible_names(hidden: Bool, usage: String, attrs: List<&2, Item>) ->
5864    List<&2, String>:
5865  match hidden:
5866    case True{}: []
5867    case False{}: option_names(parse_usage(usage), attrs)
5868
5869# T4 for E01: visible longs, their no- forms and long aliases.
5870def long_candidates(items: List<&2, Item>) -> List<&2, String>:
5871  match items:
5872    case Nil{}: Nil{}
5873    case Arg{usage, help, +attrs} <> tail:
5874      List.append(&2, String, visible_names(has_hidden(attrs), usage, attrs),
5875        long_candidates(tail))
5876    case head <> tail: long_candidates(tail)
5877
5878# T4 for E02: visible subcommand names and aliases, then help.
5879def sub_candidates(items: List<&2, Item>) -> List<&2, String>:
5880  match items:
5881    case Nil{}: ["help"]
5882    case Sub{name, about, +children} <> tail:
5883      List.append(&2, String,
5884        Bool.pick(List<&2, String>, has_hidden(children), [],
5885          name <> find_aliases(children)),
5886        sub_candidates(tail))
5887    case head <> tail: sub_candidates(tail)
5888
5889# T4 for E04: the one_of values of the arg E04 names.
5890def display_choices(items: List<&2, Item>, +arg: String,
5891    found: List<&2, String>) -> List<&2, String>:
5892  match items found:
5893    case _ head <> tail: head <> tail
5894    case Nil{} Nil{}: Nil{}
5895    case Arg{usage, help, +attrs} <> tail Nil{}:
5896      display_choices(tail, arg, Bool.pick(List<&2, String>,
5897        String.eq(arg_display(arg_info(usage, attrs)), arg),
5898        list_or_none(find_one_of(attrs)), []))
5899    case head <> tail Nil{}: display_choices(tail, arg, [])
5900
5901def owned(found: Maybe<&2, OptFound>, text: String) -> Maybe<&2, String>:
5902  match found:
5903    case None{}: None{}
5904    case Some{option}: Some{text}
5905
5906def owned_by(hit: Maybe<&2, OptFound>, token: String, prefix: String,
5907    step: String, found: Maybe<&2, String>) -> Maybe<&2, String>:
5908  match hit:
5909    case None{}: found
5910    case Some{option}:
5911      Some{String.concat(["'", token, "' belongs to '", prefix,
5912        "'; place it before '", step, "'"])}
5913
5914# T1: the deepest ancestor whose args take the long, and its child on path.
5915def owner(path: List<&2, String>, +items: List<&2, Item>, +prefix: String,
5916    +token: String, +long: String, found: Maybe<&2, String>) ->
5917    Maybe<&2, String>:
5918  match path:
5919    case Nil{}: found
5920    case +step <> tail:
5921      owner(tail, sub_items(find_sub(items, step)),
5922        String.concat([prefix, " ", step]), token, long,
5923        owned_by(find_option(items, long), token, prefix, step, found))
5924
5925# T2: the first subcommand in pre-order whose args take the long.
5926def descendant(items: List<&2, Item>, +prefix: String, +long: String,
5927    found: Maybe<&2, String>) -> Maybe<&2, String>:
5928  match items found:
5929    case _ Some{path}: Some{path}
5930    case Nil{} None{}: None{}
5931    case Sub{name, about, +children} <> tail None{}:
5932      +path = String.concat([prefix, " ", name])
5933      descendant(tail, prefix, long, descendant(children, path, long,
5934        owned(find_option(children, long), path)))
5935    case head <> tail None{}: descendant(tail, prefix, long, None{})
5936
5937def maybe_tip(found: Maybe<&2, String>) -> List<&2, List<&2, Segment>>:
5938  match found:
5939    case None{}: Nil{}
5940    case Some{text}: tip(text)
5941
5942def belongs(found: Maybe<&2, String>, token: String) ->
5943    List<&2, List<&2, Segment>>:
5944  match found:
5945    case None{}: Nil{}
5946    case Some{path}:
5947      tip(String.concat(["'", token, "' belongs to subcommand '", path, "'"]))
5948
5949def sub_exists(found: Maybe<&2, SubFound>, name: String) ->
5950    List<&2, List<&2, Segment>>:
5951  match found:
5952    case None{}: Nil{}
5953    case Some{sub}:
5954      tip(String.concat(["subcommand '", name,
5955        "' exists; to use it, remove the '--'"]))
5956
5957def before_eq(s: String) -> String:
5958  match s:
5959    case SNil{}: SNil{}
5960    case SCon{Chr{61}, tail}: SNil{}
5961    case SCon{head, tail}: SCon{head, before_eq(tail)}
5962
5963# T1, T2 and T3, for E01 on a long; --name=value looks up --name.
5964def long_tips(is_long: Bool, +token: String, root: String,
5965    path: List<&2, String>, +items: List<&2, Item>,
5966    root_items: List<&2, Item>, here: String) ->
5967    List<&2, List<&2, Segment>>:
5968  match is_long:
5969    case False{}: Nil{}
5970    case True{}:
5971      +long = before_eq(token)
5972      +name = String.drop(long, 2n)
5973      List.concat(&2, List<&2, Segment>, [
5974        maybe_tip(owner(path, root_items, root, token, long, None{})),
5975        belongs(descendant(items, here, long, None{}), token),
5976        sub_exists(find_sub(items, name), name),
5977      ])
5978
5979def operand_count(events: List<&2, ScanEvent>, count: Nat) -> Nat:
5980  match events:
5981    case Nil{}: count
5982    case Enter{name} <> tail: operand_count(tail, 0n)
5983    case Operand{token} <> tail: operand_count(tail, (count + 1n : Nat))
5984    case event <> tail: operand_count(tail, count)
5985
5986def trace_operands(trace: ScanTrace) -> Nat:
5987  match trace:
5988    case ScanTrace{path, events, ended, external, issue}:
5989      operand_count(events, 0n)
5990
5991def dash_slot(open: Bool, +token: String) -> List<&2, List<&2, Segment>>:
5992  match open:
5993    case False{}: Nil{}
5994    case True{}:
5995      tip(String.concat(["to pass '", token, "' as a value, use '-- ", token,
5996        "'"]))
5997
5998# T5: a hyphen token that the next positional slot could take after `--`;
5999# only then is argv scanned again to count the operands in scope.
6000def dash_tip(hyphen: Bool, token: String, items: List<&2, Item>, c: Cmd,
6001    argv: List<&2, String>) -> List<&2, List<&2, Segment>>:
6002  match hyphen:
6003    case False{}: Nil{}
6004    case True{}:
6005      dash_slot(positional_can_take(items, trace_operands(scan(c, argv))),
6006        token)
6007
6008def cmd_items(c: Cmd) -> List<&2, Item>:
6009  match c:
6010    case Cmd{name, about, items}: items
6011
6012# Every tip that applies, in table order.
6013def fault_tips(fault: ScanFault, +c: Cmd, +path: List<&2, String>,
6014    argv: List<&2, String>, here: String, +items: List<&2, Item>,
6015    globals: List<&2, Item>) -> List<&2, List<&2, Segment>>:
6016  match fault:
6017    case E01{+token}:
6018      List.concat(&2, List<&2, Segment>, [
6019        long_tips(long_name(token), token, cmd_name(c), path, items,
6020          cmd_items(c), here),
6021        similar("argument", suggest(token, List.concat(&2, String, [
6022          long_candidates(List.append(&2, Item, items, globals)),
6023          ["--help"],
6024          Bool.pick(List<&2, String>, Bool.and(List.is_empty(&2, String, path),
6025            has_version_item(items)), ["--version"], []),
6026        ]))),
6027        dash_tip(String.starts_with(token, "-"), token, items, c, argv),
6028      ])
6029    case E02{+name}: similar("subcommand", suggest(name, sub_candidates(items)))
6030    case E04{+arg, +value, reason}:
6031      similar("value", suggest(value,
6032        display_choices(List.append(&2, Item, items, globals), arg, [])))
6033    case E09{+token}:
6034      dash_tip(String.starts_with(token, "-"), token, items, c, argv)
6035    case _: Nil{}
6036
6037def tip_spans(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
6038  match tips:
6039    case Nil{}: Nil{}
6040    case line <> tail:
6041      List.concat(&2, Segment, [
6042        [plain("  "), Segment{"tip:", [Bold{}, Green{}]}, plain(" ")],
6043        line, [plain("\n")], tip_spans(tail)])
6044
6045def tip_block(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
6046  match tips:
6047    case Nil{}: Nil{}
6048    case head <> tail: plain("\n") <> tip_spans(head <> tail)
6049
6050def error_page(message: List<&2, Segment>,
6051    tips: List<&2, List<&2, Segment>>, usage: List<&2, Segment>) ->
6052    List<&2, Segment>:
6053  List.concat(&2, Segment, [
6054    [Segment{"error:", [Bold{}, Red{}]}, plain(" ")], message, [plain("\n")],
6055    tip_block(tips), [plain("\n")], usage,
6056    [plain("\n\nFor more information, try '"), Segment{"--help", [Bold{}]},
6057      plain("'.\n")],
6058  ])
6059
6060def fault_view(view: View, +fault: ScanFault, c: Cmd, path: List<&2, String>,
6061    argv: List<&2, String>, w: Nat) -> List<&2, Segment>:
6062  match view:
6063    case View{+here, about, +items, +globals, root, stuck}:
6064      error_page(fault_message(fault, here, items),
6065        fault_tips(fault, c, path, argv, here, items, globals),
6066        usage_block(usage_elems(here, items, globals), w))
6067
6068# An error at the scope where scanning stopped, in stderr's colors.
6069def fault_err(t: Term, fault: ScanFault, +c: Cmd, +path: List<&2, String>,
6070    argv: List<&2, String>) -> String:
6071  match t:
6072    case Term{out_color, err_color, width}:
6073      emit(err_color, fault_view(view_of(c, path), fault, c, path, argv,
6074        page_width(width)))
6075
6076# The E02 page eval prints for an unknown subcommand name at path, with its
6077# similar names and the scope's usage, in t's stderr colors and width. An app
6078# with external() prints it, and exits 2 as eval does, when no plugin answers
6079# to name.
6080def unknown_text(c: Cmd, path: List<&2, String>, name: String,
6081    t: Term) -> String:
6082  fault_err(t, E02{name}, c, path, [])
6083
6084def summary_err(t: Term, view: View) -> String:
6085  match t:
6086    case Term{out_color, err_color, width}:
6087      emit(err_color, help_page(view, False{}, page_width(width)))
6088
6089# ---- Lint lines (§9) ----
6090
6091def problem_line(p: Problem) -> String:
6092  match p:
6093    case BadUsage{at, usage, col}:
6094      String.concat(["  ", at, ": usage '", usage, "' is invalid at column ",
6095        Nat.show((col + 1n : Nat))])
6096    case BadName{at, name}:
6097      String.concat(["  ", at, ": '", name, "' is not a valid name"])
6098    case Dup{at, name}:
6099      String.concat(["  ", at, ": '", name, "' is declared more than once"])
6100    case Reserved{at, name}:
6101      String.concat(["  ", at, ": '", name, "' is reserved"])
6102    case Misplaced{at, attr}:
6103      String.concat(["  ", at, ": '", attr, "' is not allowed here"])
6104    case NoHelp{at, what}:
6105      String.concat(["  ", at, ": '", what, "' has no help text"])
6106    case BadDefault{at, id, value}:
6107      String.concat(["  ", at, ": '", id, "' cannot have the default '", value,
6108        "'"])
6109    case BadRef{at, id}:
6110      String.concat(["  ", at, ": '", id,
6111        "' is an unknown id or an unused or empty group"])
6112    case BadPositional{at, id, why}:
6113      String.concat(["  ", at, ": positional '", id, "': ", why])
6114    case Mixed{at}:
6115      String.concat(["  ", at, ": has both positionals and subcommands"])
6116    case BadChoices{at, id}:
6117      String.concat(["  ", at, ": one_of for '", id,
6118        "' is empty or repeats a value"])
6119    case BadChoice{at, id, value}:
6120      String.concat(["  ", at, ": '", id, "' cannot have the choice '", value,
6121        "'"])
6122    case NeedBare{at, id}:
6123      String.concat(["  ", at, ": '", id,
6124        "' needs [=<V>] and bare() together, with no short"])
6125    case DecodeBug{at, msg}:
6126      String.concat(["  ", at, ": decoder bug: ", msg])
6127
6128def problem_lines(found: List<&2, Problem>) -> String:
6129  match found:
6130    case Nil{}: ""
6131    case p <> tail: String.concat([problem_line(p), "\n", problem_lines(tail)])
6132
6133# ---- Completions and man pages (§11) ----
6134
6135type Shell is Data:
6136  Bash{}
6137  Zsh{}
6138  Fish{}
6139
6140def sh_body(s: String) -> String:
6141  match s:
6142    case SNil{}: SNil{}
6143    case SCon{Chr{39}, tail}: String.append("'\\''", sh_body(tail))
6144    case SCon{c, tail}: SCon{c, sh_body(tail)}
6145
6146# POSIX single quotes, with ' written as '\'' (P3).
6147def Sh.quote(s: String) -> String:
6148  SCon{'\'', String.append(sh_body(s), "'")}
6149
6150def fish_body(s: String) -> String:
6151  match s:
6152    case SNil{}: SNil{}
6153    case SCon{Chr{39}, tail}: String.append("\\'", fish_body(tail))
6154    case SCon{Chr{92}, tail}: String.append("\\\\", fish_body(tail))
6155    case SCon{c, tail}: SCon{c, fish_body(tail)}
6156
6157# fish single quotes, with \ and ' backslash-escaped (P4).
6158def Fish.quote(s: String) -> String:
6159  SCon{'\'', String.append(fish_body(s), "'")}
6160
6161# Text in an _arguments spec or a _describe entry, with \, [, ] and :
6162# backslash-escaped (P5).
6163def Zsh.esc(s: String) -> String:
6164  match s:
6165    case SNil{}: SNil{}
6166    case SCon{Chr{92}, tail}: String.append("\\\\", Zsh.esc(tail))
6167    case SCon{Chr{91}, tail}: String.append("\\[", Zsh.esc(tail))
6168    case SCon{Chr{93}, tail}: String.append("\\]", Zsh.esc(tail))
6169    case SCon{Chr{58}, tail}: String.append("\\:", Zsh.esc(tail))
6170    case SCon{c, tail}: SCon{c, Zsh.esc(tail)}
6171
6172# Text for man(7), start telling whether a line starts here: \, -, ", '
6173# and ` as escapes, and \& before a . or a space that starts a line.
6174def roff_body(s: String, start: Bool) -> String:
6175  match s start:
6176    case SNil{} _: SNil{}
6177    case SCon{Chr{46}, tail} True{}: String.append("\\&.", roff_body(tail, False{}))
6178    case SCon{Chr{32}, tail} True{}: String.append("\\& ", roff_body(tail, False{}))
6179    case SCon{Chr{10}, tail} _: SCon{'\n', roff_body(tail, True{})}
6180    case SCon{Chr{92}, tail} _: String.append("\\e", roff_body(tail, False{}))
6181    case SCon{Chr{45}, tail} _: String.append("\\-", roff_body(tail, False{}))
6182    case SCon{Chr{34}, tail} _: String.append("\\(dq", roff_body(tail, False{}))
6183    case SCon{Chr{39}, tail} _: String.append("\\(aq", roff_body(tail, False{}))
6184    case SCon{Chr{96}, tail} _: String.append("\\(ga", roff_body(tail, False{}))
6185    case SCon{c, tail} _: SCon{c, roff_body(tail, False{})}
6186
6187# Text that starts lines (P6, P15).
6188def Roff.esc(s: String) -> String:
6189  roff_body(s, True{})
6190
6191# Text after the start of a line.
6192def roff_text(s: String) -> String:
6193  roff_body(s, False{})
6194
6195def text_lines(lines: List<&2, String>) -> String:
6196  match lines:
6197    case Nil{}: ""
6198    case line <> tail: String.concat([line, "\n", text_lines(tail)])
6199
6200# The summary with its hard line breaks as spaces.
6201def one_line(s: String) -> String:
6202  String.join(String.lines(summary(s)), " ")
6203
6204def ident_char(+c: Char) -> Char:
6205  Bool.pick(Char, Bool.or(name_start(c), Char.is_eq(c, '_')), c, '_')
6206
6207# A name with every char outside [A-Za-z0-9_] as _, for shell identifiers.
6208def ident(s: String) -> String:
6209  match s:
6210    case SNil{}: SNil{}
6211    case SCon{c, tail}: SCon{ident_char(c), ident(tail)}
6212
6213def same_words(a: List<&2, String>, b: List<&2, String>) -> Bool:
6214  match a b:
6215    case Nil{} Nil{}: True{}
6216    case x <> xs y <> ys: Bool.and(String.eq(x, y), same_words(xs, ys))
6217    case _ _: False{}
6218
6219def last_with(lines: List<&2, String>, +suffix: String) -> List<&2, String>:
6220  match lines:
6221    case Nil{}: Nil{}
6222    case line <> Nil{}: [String.append(line, suffix)]
6223    case line <> tail: line <> last_with(tail, suffix)
6224
6225# A command on a visible path, in pre-order from the root: the words of its
6226# path below the root, its items, and the globals it inherits, root first.
6227type Place is Data:
6228  Place{words: List<&2, String>, items: List<&2, Item>,
6229    globals: List<&2, Item>}
6230
6231# The visible commands below items, whose subcommands inherit globals.
6232def places(items: List<&2, Item>, +words: List<&2, String>,
6233    +globals: List<&2, Item>) -> List<&2, Place>:
6234  match items:
6235    case Nil{}: Nil{}
6236    case Sub{name, about, +children} <> tail:
6237      +here = List.append(&2, String, words, [name])
6238      List.append(&2, Place,
6239        Bool.pick(List<&2, Place>, has_hidden(children), [],
6240          Place{here, children, globals} <> places(children, here,
6241            List.append(&2, Item, globals, global_items(children)))),
6242        places(tail, words, globals))
6243    case head <> tail: places(tail, words, globals)
6244
6245def all_places(+items: List<&2, Item>) -> List<&2, Place>:
6246  Place{[], items, []} <> places(items, [], global_items(items))
6247
6248# What a value completes to: a word of the one_of list numbered list, a
6249# file, a directory, or nothing.
6250type Comp is Data:
6251  Words{list: Nat, values: List<&2, String>}
6252  Files{}
6253  Dirs{}
6254  Free{}
6255
6256# An option as a script offers it: its names, shorts first, its kind, its
6257# placeholder, what its value completes to, and its help on one line.
6258type Opt is Data:
6259  Opt{names: List<&2, String>, kind: UsageKind, ph: String, comp: Comp,
6260    help: String}
6261
6262# A positional slot, numbered from 1.
6263type Slot is Data:
6264  Slot{k: Nat, hidden: Bool, required: Bool, many: Bool, rest: Bool,
6265    ph: String, comp: Comp}
6266
6267# A visible subcommand: its name, its names with aliases, and its summary.
6268type Entry is Data:
6269  Entry{name: String, names: List<&2, String>, about: String}
6270
6271# The number of values in lists, counting from k + 1 after a miss, or 0.
6272def list_number(lists: List<&2, List<&2, String>>,
6273    +values: List<&2, String>, hit: Bool, +k: Nat) -> Nat:
6274  match lists hit:
6275    case _ True{}: k
6276    case Nil{} False{}: 0n
6277    case head <> tail False{}:
6278      list_number(tail, values, same_words(head, values), (k + 1n : Nat))
6279
6280def add_list(found: Maybe<&2, List<&2, String>>,
6281    +lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6282  match found:
6283    case None{}: lists
6284    case Some{+values}:
6285      Bool.pick(List<&2, List<&2, String>>,
6286        List.contains(~List<&2, String>, ~same_words, lists, values), lists,
6287        List.append(&2, List<&2, String>, lists, [values]))
6288
6289def visible_one_of(hidden: Bool, attrs: List<&2, Item>) ->
6290    Maybe<&2, List<&2, String>>:
6291  match hidden:
6292    case True{}: None{}
6293    case False{}: find_one_of(attrs)
6294
6295def item_lists(items: List<&2, Item>, lists: List<&2, List<&2, String>>) ->
6296    List<&2, List<&2, String>>:
6297  match items:
6298    case Nil{}: lists
6299    case Arg{usage, help, +attrs} <> tail:
6300      item_lists(tail, add_list(visible_one_of(has_hidden(attrs), attrs), lists))
6301    case head <> tail: item_lists(tail, lists)
6302
6303# The one_of lists of visible args, each once, in pre-order.
6304def place_lists(places: List<&2, Place>,
6305    lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6306  match places:
6307    case Nil{}: lists
6308    case Place{words, items, globals} <> tail:
6309      place_lists(tail, item_lists(items, lists))
6310
6311def type_comp(found: Maybe<&2, ValueType>) -> Comp:
6312  match found:
6313    case Some{TFile{}}: Files{}
6314    case Some{TDir{}}: Dirs{}
6315    case _: Free{}
6316
6317def comp_of(found: Maybe<&2, List<&2, String>>, t: Maybe<&2, ValueType>,
6318    lists: List<&2, List<&2, String>>) -> Comp:
6319  match found:
6320    case Some{+values}: Words{list_number(lists, values, False{}, 0n), values}
6321    case None{}: type_comp(t)
6322
6323def head_part(parts: List<&2, String>) -> String:
6324  match parts:
6325    case head <> tail: head
6326    case Nil{}: ""
6327
6328def next_part(parts: List<&2, String>) -> String:
6329  match parts:
6330    case head <> next <> tail: next
6331    case _: ""
6332
6333def between(s: String, +open: Char, close: Char) -> String:
6334  head_part(String.split(next_part(String.split(s, open)), close))
6335
6336# The name in a usage's <>, or in an optional positional's [].
6337def placeholder(+usage: String) -> String:
6338  Bool.pick(String, String.contains(usage, "<"), between(usage, '<', '>'),
6339    between(usage, '[', ']'))
6340
6341def short_alias(s: String) -> Bool:
6342  Bool.not(long_name(s))
6343
6344def short_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
6345  match parsed:
6346    case Parsed{id, +short, long, kind}:
6347      List.append(&2, String,
6348        Bool.pick(List<&2, String>, String.is_empty(short), [], [short]),
6349        List.filter(~String, ~short_alias, find_aliases(attrs)))
6350    case Invalid{col}: []
6351
6352def parsed_kind(parsed: Usage) -> UsageKind:
6353  match parsed:
6354    case Parsed{id, short, long, kind}: kind
6355    case Invalid{col}: FlagKind{}
6356
6357def arg_opt(+parsed: Usage, usage: String, help: String,
6358    +attrs: List<&2, Item>, lists: List<&2, List<&2, String>>) -> Opt:
6359  Opt{List.append(&2, String, short_names(parsed, attrs),
6360      option_names(parsed, attrs)),
6361    parsed_kind(parsed), placeholder(usage),
6362    comp_of(find_one_of(attrs), find_as(attrs), lists), one_line(help)}
6363
6364# The visible options in items.
6365def arg_opts(items: List<&2, Item>, +lists: List<&2, List<&2, String>>) ->
6366    List<&2, Opt>:
6367  match items:
6368    case Nil{}: Nil{}
6369    case Arg{+usage, help, +attrs} <> tail:
6370      +rest = arg_opts(tail, lists)
6371      +parsed = parse_usage(usage)
6372      Bool.pick(List<&2, Opt>,
6373        Bool.or(usage_kind(~kind_pos, parsed), has_hidden(attrs)), rest,
6374        arg_opt(parsed, usage, help, attrs, lists) <> rest)
6375    case head <> tail: arg_opts(tail, lists)
6376
6377def flag_opt(names: List<&2, String>, help: String) -> Opt:
6378  Opt{names, FlagKind{}, "", Free{}, help}
6379
6380def version_opts(present: Bool, args: List<&2, Item>) -> List<&2, Opt>:
6381  match present:
6382    case True{}:
6383      [flag_opt(Bool.pick(List<&2, String>,
6384        Maybe.is_some(&2, OptFound, find_option(args, "-V")),
6385        ["--version"], ["-V", "--version"]), "Print version")]
6386    case False{}: []
6387
6388# -h and --help, without -h when an arg took it, then at a root with a
6389# version -V and --version likewise.
6390def builtin_opts(+items: List<&2, Item>, globals: List<&2, Item>,
6391    root: Bool) -> List<&2, Opt>:
6392  +args = List.append(&2, Item, items, globals)
6393  flag_opt(Bool.pick(List<&2, String>,
6394      Maybe.is_some(&2, OptFound, find_option(args, "-h")),
6395      ["--help"], ["-h", "--help"]), "Print help") <>
6396    version_opts(Bool.and(root, has_version_item(items)), args)
6397
6398def pos_slot(parsed: Usage, k: Nat, usage: String, +attrs: List<&2, Item>,
6399    lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6400  match parsed:
6401    case Parsed{id, short, long, PosKind{required, many}}:
6402      [Slot{k, has_hidden(attrs), required, many, has_rest(attrs),
6403        placeholder(usage), comp_of(find_one_of(attrs), find_as(attrs), lists)}]
6404    case _: []
6405
6406# The positional slots in items, hidden ones too, numbered from k.
6407def arg_slots(items: List<&2, Item>, +k: Nat,
6408    +lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6409  match items:
6410    case Nil{}: Nil{}
6411    case Arg{+usage, help, attrs} <> tail:
6412      +parsed = parse_usage(usage)
6413      List.append(&2, Slot, pos_slot(parsed, k, usage, attrs, lists),
6414        arg_slots(tail, Bool.pick(Nat, usage_kind(~kind_pos, parsed),
6415          (k + 1n : Nat), k), lists))
6416    case head <> tail: arg_slots(tail, k, lists)
6417
6418# The slots up to the variadic one, which takes every later value.
6419def completing(slots: List<&2, Slot>) -> List<&2, Slot>:
6420  match slots:
6421    case Nil{}: Nil{}
6422    case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6423      [Slot{k, hidden, required, True{}, rest, ph, comp}]
6424    case head <> tail: head <> completing(tail)
6425
6426def variadic(slots: List<&2, Slot>) -> Maybe<&2, Slot>:
6427  match slots:
6428    case Nil{}: None{}
6429    case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6430      Some{Slot{k, hidden, required, True{}, rest, ph, comp}}
6431    case head <> tail: variadic(tail)
6432
6433# The visible subcommands in items.
6434def sub_entries(items: List<&2, Item>) -> List<&2, Entry>:
6435  match items:
6436    case Nil{}: Nil{}
6437    case Sub{+name, about, +children} <> tail:
6438      +rest = sub_entries(tail)
6439      Bool.pick(List<&2, Entry>, has_hidden(children), rest,
6440        Entry{name, name <> find_aliases(children), one_line(about)} <> rest)
6441    case head <> tail: sub_entries(tail)
6442
6443def opt_names(opts: List<&2, Opt>) -> List<&2, String>:
6444  match opts:
6445    case Nil{}: Nil{}
6446    case Opt{names, kind, ph, comp, help} <> tail:
6447      List.append(&2, String, names, opt_names(tail))
6448
6449def entry_names(entries: List<&2, Entry>) -> List<&2, String>:
6450  match entries:
6451    case Nil{}: Nil{}
6452    case Entry{name, names, about} <> tail:
6453      List.append(&2, String, names, entry_names(tail))
6454
6455# What a script needs at a place: its number, its words, its options in
6456# scope, own first, the built-ins, its visible subcommands, and its slots.
6457type Stop is Data:
6458  Stop{k: Nat, words: List<&2, String>, opts: List<&2, Opt>,
6459    builtins: List<&2, Opt>, subs: List<&2, Entry>, slots: List<&2, Slot>}
6460
6461def stop_of(place: Place, k: Nat, +lists: List<&2, List<&2, String>>) ->
6462    Stop:
6463  match place:
6464    case Place{+words, +items, +globals}:
6465      Stop{k, words, List.append(&2, Opt, arg_opts(items, lists),
6466          arg_opts(globals, lists)),
6467        builtin_opts(items, globals, List.is_empty(&2, String, words)),
6468        sub_entries(items), arg_slots(items, 1n, lists)}
6469
6470def stops(places: List<&2, Place>, +k: Nat,
6471    +lists: List<&2, List<&2, String>>) -> List<&2, Stop>:
6472  match places:
6473    case Nil{}: Nil{}
6474    case place <> tail:
6475      stop_of(place, k, lists) <> stops(tail, (k + 1n : Nat), lists)
6476
6477# The number of the stop at words, from found on.
6478def stop_number(stops: List<&2, Stop>, +words: List<&2, String>,
6479    found: Maybe<&2, Nat>) -> Nat:
6480  match stops found:
6481    case _ Some{k}: k
6482    case Nil{} None{}: 0n
6483    case Stop{+k, here, opts, builtins, subs, slots} <> tail None{}:
6484      stop_number(tail, words, Bool.pick(Maybe<&2, Nat>,
6485        same_words(here, words), Some{k}, None{}))
6486
6487# ---- bash
6488
6489# Words quoted for sh, apart by sep.
6490def sh_list(words: List<&2, String>, +sep: String) -> String:
6491  match words:
6492    case Nil{}: ""
6493    case word <> Nil{}: Sh.quote(word)
6494    case word <> tail: String.concat([Sh.quote(word), sep, sh_list(tail, sep)])
6495
6496def dests(names: List<&2, String>, +dest: String) -> List<&2, String>:
6497  match names:
6498    case Nil{}: Nil{}
6499    case name <> tail: dest <> dests(tail, dest)
6500
6501# Each subcommand name's path, once per name.
6502def entry_dests(entries: List<&2, Entry>, +words: List<&2, String>) ->
6503    List<&2, String>:
6504  match entries:
6505    case Nil{}: Nil{}
6506    case Entry{name, names, about} <> tail:
6507      List.append(&2, String,
6508        dests(names, String.join(List.append(&2, String, words, [name]), " ")),
6509        entry_dests(tail, words))
6510
6511def bash_subs(+entries: List<&2, Entry>, words: List<&2, String>) ->
6512    List<&2, String>:
6513  Bool.pick(List<&2, String>, List.is_empty(&2, Entry, entries), [], [
6514    String.concat(["      subs=(", sh_list(entry_names(entries), " "), ")"]),
6515    String.concat(["      dests=(", sh_list(entry_dests(entries, words), " "),
6516      ")"]),
6517  ])
6518
6519def bash_shape(found: Maybe<&2, Slot>) -> List<&2, String>:
6520  match found:
6521    case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
6522      [String.concat(["      many=", Nat.show(k), " rest=",
6523        Bool.pick(String, rest, "1", "0")])]
6524    case None{}: []
6525
6526def bash_scope_arm(stop: Stop) -> List<&2, String>:
6527  match stop:
6528    case Stop{k, +words, opts, builtins, subs, slots}:
6529      String.concat(["    ", Sh.quote(String.join(words, " ")), ")"]) <>
6530        last_with(List.concat(&2, String, [
6531          [String.concat(["      opts=(", sh_list(List.append(&2, String,
6532            opt_names(opts), opt_names(builtins)), " "), ")"])],
6533          bash_subs(subs, words),
6534          bash_shape(variadic(slots)),
6535        ]), " ;;")
6536
6537def bash_letter(comp: Comp) -> String:
6538  match comp:
6539    case Words{list, values}: "w"
6540    case Files{}: "f"
6541    case Dirs{}: "d"
6542    case Free{}: "s"
6543
6544def bash_values(comp: Comp) -> String:
6545  match comp:
6546    case Words{list, values}: String.concat([" values=(", sh_list(values, " "), ")"])
6547    case _: ""
6548
6549def bash_kind(+comp: Comp, bare: Bool) -> String:
6550  String.concat(["kind=", bash_letter(comp),
6551    Bool.pick(String, bare, " opt=1", ""), bash_values(comp)])
6552
6553def bash_opt_case(opt: Opt) -> List<&2, String>:
6554  match opt:
6555    case Opt{names, OptionKind{many}, ph, comp, help}:
6556      [String.concat(["        ", sh_list(names, "|"), ") ",
6557        bash_kind(comp, False{}), " ;;"])]
6558    case Opt{names, BareOptionKind{}, ph, comp, help}:
6559      [String.concat(["        ", sh_list(names, "|"), ") ",
6560        bash_kind(comp, True{}), " ;;"])]
6561    case _: []
6562
6563def bash_opt_cases(opts: List<&2, Opt>) -> List<&2, String>:
6564  match opts:
6565    case Nil{}: Nil{}
6566    case opt <> tail: List.append(&2, String, bash_opt_case(opt), bash_opt_cases(tail))
6567
6568def bash_slot_cases(slots: List<&2, Slot>) -> List<&2, String>:
6569  match slots:
6570    case Nil{}: Nil{}
6571    case Slot{k, False{}, required, many, rest, ph, Words{list, values}} <> tail:
6572      String.concat(["        ", Nat.show(k), ") ",
6573        bash_kind(Words{list, values}, False{}), " ;;"]) <> bash_slot_cases(tail)
6574    case Slot{k, False{}, required, many, rest, ph, Files{}} <> tail:
6575      String.concat(["        ", Nat.show(k), ") kind=f ;;"]) <> bash_slot_cases(tail)
6576    case Slot{k, False{}, required, many, rest, ph, Dirs{}} <> tail:
6577      String.concat(["        ", Nat.show(k), ") kind=d ;;"]) <> bash_slot_cases(tail)
6578    case head <> tail: bash_slot_cases(tail)
6579
6580def bash_cases(words: List<&2, String>, cases: List<&2, String>) ->
6581    List<&2, String>:
6582  match cases:
6583    case Nil{}: []
6584    case head <> tail:
6585      List.concat(&2, String, [
6586        [String.concat(["    ", Sh.quote(String.join(words, " ")), ")"]),
6587          "      case $1 in"],
6588        head <> tail,
6589        ["      esac ;;"],
6590      ])
6591
6592def bash_arg_arm(stop: Stop) -> List<&2, String>:
6593  match stop:
6594    case Stop{k, words, opts, builtins, subs, slots}:
6595      bash_cases(words, List.append(&2, String, bash_opt_cases(opts),
6596        bash_slot_cases(completing(slots))))
6597
6598def bash_arms(~f: Stop -> List<&2, String>, stops: List<&2, Stop>) ->
6599    List<&2, String>:
6600  match stops:
6601    case Nil{}: Nil{}
6602    case stop <> tail: List.append(&2, String, f(stop), bash_arms(~f, tail))
6603# The rest of every bash script, for function prefix id and the quoted
6604# command name: the parser of COMP_WORDS, then the binding.
6605def bash_engine(+id: String, name: String) -> List<&2, String>:
6606  [
6607    "",
6608    "# Offers each value of kind that starts with $1, written after $2 and cut to",
6609    "# the part readline completes: the text after the last \"=\" or \":\". Readline",
6610    "# quotes files and directories; words are quoted here.",
6611    String.concat([id, "_offer() {"]),
6612    "  local word full i",
6613    "  case $kind in",
6614    "    f) compopt -o filenames; mapfile -t values < <(compgen -f -- \"$1\") ;;",
6615    "    d) compopt -o filenames; mapfile -t values < <(compgen -d -- \"$1\") ;;",
6616    "    w)",
6617    "      for ((i = 0; i < ${#values[@]}; i++)); do",
6618    "        printf -v 'values[i]' %q \"${values[i]}\"",
6619    "      done ;;",
6620    "    *) return ;;",
6621    "  esac",
6622    "  for word in \"${values[@]}\"; do",
6623    "    full=$2$word",
6624    "    [[ $word == \"$1\"* ]] && COMPREPLY+=(\"${full:${#2}+${#1}-${#tail}}\")",
6625    "  done",
6626    "}",
6627    "",
6628    String.concat([id, "() {"]),
6629    "  local -a words=() opts=() subs=() dests=() values=()",
6630    "  local cur tail word line gap path= want= kind opt many rest i j n=0 end=0 help=0",
6631    "  COMPREPLY=()",
6632    "  # Rejoin the \"=\" and \":\" that bash splits words at. COMP_LINE has no blank",
6633    "  # at such a split, so a \"=\" typed as its own word stays one.",
6634    "  line=${COMP_LINE#*\"${COMP_WORDS[0]}\"}",
6635    "  for ((i = 1; i <= COMP_CWORD; i++)); do",
6636    "    word=${COMP_WORDS[i]}",
6637    "    tail=${COMP_WORDS[i-1]}",
6638    "    gap=${line%%[![:blank:]]*}",
6639    "    line=${line:${#gap}+${#word}}",
6640    "    if ((i > 1)) && [[ -z $gap ]] &&",
6641    "      [[ -n $word && -z ${word//[=:]} || -n $tail && -z ${tail//[=:]} ]]; then",
6642    "      words[${#words[@]}-1]+=$word",
6643    "    else",
6644    "      words+=(\"$word\")",
6645    "    fi",
6646    "  done",
6647    "  tail=${COMP_WORDS[COMP_CWORD]}",
6648    "  [[ -z ${tail//[=:]} ]] && tail=",
6649    "  cur=${words[${#words[@]}-1]}",
6650    "  unset 'words[${#words[@]}-1]'",
6651    String.concat(["  ", id, "_scope"]),
6652    "  for word in \"${words[@]}\"; do",
6653    "    if [[ -n $want ]]; then",
6654    "      want=",
6655    "    elif ((!end)) && [[ $word == -- ]]; then",
6656    "      end=1",
6657    "    elif ((!end)) && [[ $word == --* ]]; then",
6658    String.concat(["      ", id, "_arg \"${word%%=*}\""]),
6659    "      [[ -n $kind && $opt == 0 && $word != *=* ]] && want=$word",
6660    "    elif ((!end)) && [[ $word == -?* ]]; then",
6661    "      for ((j = 1; j < ${#word}; j++)); do",
6662    String.concat(["        ", id, "_arg \"-${word:j:1}\""]),
6663    "        [[ -n $kind ]] || continue",
6664    "        ((j + 1 == ${#word})) && want=-${word:j:1}",
6665    "        break",
6666    "      done",
6667    "    elif ((${#subs[@]})); then",
6668    "      ((end)) && return",
6669    "      if [[ $word == help ]] && ((!help)); then",
6670    "        help=1",
6671    "        continue",
6672    "      fi",
6673    "      for ((j = 0; j < ${#subs[@]}; j++)); do",
6674    "        [[ $word == \"${subs[j]}\" ]] && break",
6675    "      done",
6676    "      ((j < ${#subs[@]})) || return",
6677    "      path=${dests[j]}",
6678    String.concat(["      ", id, "_scope"]),
6679    "    else",
6680    "      # n counts the slots filled. The variadic slot stays open, but a rest()",
6681    "      # value takes every later word, so nothing more completes.",
6682    "      n=$((n + 1))",
6683    "      if ((n == many && rest)); then",
6684    "        end=1",
6685    "      elif ((n == many)); then",
6686    "        n=$((n - 1))",
6687    "      fi",
6688    "    fi",
6689    "  done",
6690    "  if [[ -n $want ]]; then",
6691    String.concat(["    ", id, "_arg \"$want\""]),
6692    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6693    "  elif ((!end)) && [[ $cur == --*=* ]]; then",
6694    String.concat(["    ", id, "_arg \"${cur%%=*}\""]),
6695    String.concat(["    ", id, "_offer \"${cur#*=}\" \"${cur%%=*}=\""]),
6696    "  elif ((!end)) && [[ $cur == -* ]]; then",
6697    "    values=(\"${opts[@]}\") kind=w",
6698    "    ((help)) && values=('-h' '--help')",
6699    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6700    "  elif ((!end && ${#subs[@]})); then",
6701    "    values=(\"${subs[@]}\") kind=w",
6702    "    ((help)) || values+=('help')",
6703    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6704    "  elif ((!help)); then",
6705    String.concat(["    ", id, "_arg $((n + 1))"]),
6706    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6707    "  fi",
6708    "}",
6709    "",
6710    String.concat(["complete -F ", id, " ", name]),
6711  ]
6712
6713def bash_script(+name: String, +items: List<&2, Item>) -> String:
6714  +all = all_places(items)
6715  +found = stops(all, 0n, place_lists(all, []))
6716  +id = String.append("_", ident(name))
6717  text_lines(List.concat(&2, String, [
6718    ["# bash completion; needs bash 4 or later.",
6719      "",
6720      "# Sets the tables of command path $path: opts, the options to offer; subs and",
6721      "# dests, each subcommand name and the path it enters; many, the slot of the",
6722      "# variadic positional, or 0; rest, 1 when that slot is rest().",
6723      String.append(id, "_scope() {"),
6724      "  opts=() subs=() dests=() many=0 rest=0",
6725      "  case $path in"],
6726    bash_arms(~bash_scope_arm, found),
6727    ["  esac",
6728      "}",
6729      "",
6730      "# Sets kind to what the option or positional $1 (a name, or a 1-based slot)",
6731      "# takes at $path: w, a word from values; f, a file; d, a directory; s, other",
6732      "# text. kind is empty for an option that takes no value and for a positional",
6733      "# that completes nothing; opt is 1 when the value can only follow \"=\".",
6734      String.append(id, "_arg() {"),
6735      "  kind= opt=0 values=()",
6736      "  case $path in"],
6737    bash_arms(~bash_arg_arm, found),
6738    ["  esac", "}"],
6739    bash_engine(id, Sh.quote(name)),
6740  ]))
6741
6742# ---- zsh
6743
6744def zsh_action(comp: Comp) -> String:
6745  match comp:
6746    case Words{list, values}: String.concat(["($v", Nat.show(list), ")"])
6747    case Files{}: "_files"
6748    case Dirs{}: "_files -/"
6749    case Free{}: " "
6750
6751# The prefix of an option's specs: * when it repeats, else its names as a
6752# group that excludes each other.
6753def zsh_pre(kind: UsageKind, +names: List<&2, String>) -> String:
6754  match kind:
6755    case CountKind{}: "*"
6756    case OptionKind{True{}}: "*"
6757    case _:
6758      Bool.pick(String, Nat.is_lt(1n, List.length(&2, String, names)),
6759        String.concat(["(", String.join(names, " "), ")"]), "")
6760
6761def zsh_spec(+name: String, kind: UsageKind, pre: String, d: String,
6762    ph: String, act: String) -> String:
6763  match kind:
6764    case OptionKind{many}:
6765      String.concat([pre, name, Bool.pick(String, long_name(name), "=", "+"),
6766        "[", d, "]:", ph, ":", act])
6767    case BareOptionKind{}:
6768      String.concat([pre, name, "=-[", d, "]::", ph, ":", act])
6769    case _: String.concat([pre, name, "[", d, "]"])
6770
6771def zsh_names(names: List<&2, String>, +kind: UsageKind, +pre: String,
6772    +d: String, +ph: String, +act: String) -> List<&2, String>:
6773  match names:
6774    case Nil{}: Nil{}
6775    case name <> tail:
6776      zsh_spec(name, kind, pre, d, ph, act) <>
6777        zsh_names(tail, kind, pre, d, ph, act)
6778
6779def zsh_opts(opts: List<&2, Opt>) -> List<&2, String>:
6780  match opts:
6781    case Nil{}: Nil{}
6782    case Opt{+names, +kind, ph, comp, help} <> tail:
6783      List.append(&2, String,
6784        zsh_names(names, kind, zsh_pre(kind, names), Zsh.esc(help),
6785          Zsh.esc(ph), zsh_action(comp)),
6786        zsh_opts(tail))
6787
6788def zsh_slots(slots: List<&2, Slot>) -> List<&2, String>:
6789  match slots:
6790    case Nil{}: Nil{}
6791    case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
6792      zsh_slots(tail)
6793    case Slot{k, False{}, required, True{}, False{}, ph, comp} <> tail:
6794      String.concat(["*:", Zsh.esc(ph), ":", zsh_action(comp)]) <>
6795        zsh_slots(tail)
6796    case Slot{k, False{}, required, +many, rest, ph, comp} <> tail:
6797      String.concat([Bool.pick(String, many, "(-)", ""), Nat.show(k),
6798        Bool.pick(String, required, ":", "::"), Zsh.esc(ph), ":",
6799        zsh_action(comp)]) <> zsh_slots(tail)
6800
6801# Specs quoted, one per line, all but the last continued.
6802def zsh_spec_lines(specs: List<&2, String>, +ind: String) ->
6803    List<&2, String>:
6804  match specs:
6805    case Nil{}: Nil{}
6806    case spec <> Nil{}: [String.concat([ind, "  ", Sh.quote(spec)])]
6807    case spec <> tail:
6808      String.concat([ind, "  ", Sh.quote(spec), " \\"]) <>
6809        zsh_spec_lines(tail, ind)
6810
6811def zsh_args(+ind: String, specs: List<&2, String>) -> List<&2, String>:
6812  String.append(ind, "_arguments -C -s -S \\") <> zsh_spec_lines(specs, ind)
6813
6814def zsh_entry(names: List<&2, String>, +ind: String, +about: String) ->
6815    List<&2, String>:
6816  match names:
6817    case Nil{}: Nil{}
6818    case name <> tail:
6819      String.concat([ind, "  ", Sh.quote(String.concat([Zsh.esc(name), ":",
6820        about]))]) <> zsh_entry(tail, ind, about)
6821
6822def zsh_entries(entries: List<&2, Entry>, +ind: String) -> List<&2, String>:
6823  match entries:
6824    case Nil{}: Nil{}
6825    case Entry{name, names, +about} <> tail:
6826      List.append(&2, String, zsh_entry(names, ind, Zsh.esc(about)),
6827        zsh_entries(tail, ind))
6828
6829def zsh_commands(entries: List<&2, Entry>, with_help: Bool, label: String,
6830    +ind: String) -> List<&2, String>:
6831  List.concat(&2, String, [
6832    [String.append(ind, "local -a commands=(")],
6833    zsh_entries(entries, ind),
6834    Bool.pick(List<&2, String>, with_help, [String.append(ind,
6835      "  'help:Print this message or the help of the given subcommand(s)'")],
6836      []),
6837    [String.append(ind, ")"),
6838      String.concat([ind, "_describe -t commands ", Sh.quote(label),
6839        " commands"])],
6840  ])
6841
6842def zsh_label(+name: String, words: List<&2, String>) -> String:
6843  String.append(String.join(name <> words, " "), " command")
6844
6845def zsh_deeper(arms: List<&2, String>, +depth: Nat, +ind: String) ->
6846    List<&2, String>:
6847  match arms:
6848    case Nil{}: []
6849    case head <> tail:
6850      List.concat(&2, String, [
6851        [String.append(ind, "else"),
6852          String.concat([ind, "  case $words[", Nat.show(depth), "] in"])],
6853        head <> tail,
6854        [String.append(ind, "  esac")],
6855      ])
6856
6857# After help at depth, the subcommand names, then deeper by arms.
6858def zsh_help_tree(entries: List<&2, Entry>, label: String,
6859    arms: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6860  List.concat(&2, String, [
6861    [String.concat([ind, "if (( CURRENT == ", Nat.show(depth), " )); then"])],
6862    zsh_commands(entries, False{}, label, String.append(ind, "  ")),
6863    zsh_deeper(arms, depth, ind),
6864    [String.append(ind, "fi")],
6865  ])
6866
6867def bar_names(names: List<&2, String>) -> String:
6868  match names:
6869    case Nil{}: ""
6870    case name <> Nil{}: Sh.quote(name)
6871    case name <> tail: String.concat([Sh.quote(name), "|", bar_names(tail)])
6872
6873# The help tree's arms at ind for the subcommands in items that have
6874# subcommands of their own.
6875def zsh_help_arms(items: List<&2, Item>, +name: String,
6876    +words: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6877  match items:
6878    case Nil{}: Nil{}
6879    case Sub{+sub, about, +children} <> tail:
6880      +here = List.append(&2, String, words, [sub])
6881      +entries = sub_entries(children)
6882      List.append(&2, String,
6883        Bool.pick(List<&2, String>,
6884          Bool.or(has_hidden(children), List.is_empty(&2, Entry, entries)), [],
6885          String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6886            last_with(zsh_help_tree(entries, zsh_label(name, here),
6887              zsh_help_arms(children, name, here, (depth + 1n : Nat),
6888                String.append(ind, "      ")),
6889              (depth + 1n : Nat), String.append(ind, "  ")), " ;;")),
6890        zsh_help_arms(tail, name, words, depth, ind))
6891    case head <> tail: zsh_help_arms(tail, name, words, depth, ind)
6892
6893# Ends a state that _arguments entered past a "--" it dropped, after which
6894# no subcommand follows.
6895def zsh_past_end(ind: String) -> String:
6896  String.append(ind, "(( ${#${(M)words[1,CURRENT-1]:#--}} < ends )) && return 1")
6897
6898# At the top command (no words), the count of "--" before the cursor that
6899# zsh_past_end compares with.
6900def zsh_ends(words: List<&2, String>, +ind: String) -> List<&2, String>:
6901  match words:
6902    case Nil{}:
6903      [String.append(ind,
6904        "# _arguments -S drops a -- typed before a command's first word, so a"),
6905        String.append(ind,
6906          "# state whose words hold fewer -- than the line has is past one."),
6907        String.append(ind, "local -i ends=${#${(M)words[2,CURRENT-1]:#--}}")]
6908    case _: []
6909
6910# The body of a command with subcommands: its specs end in a state that
6911# offers them, completes help's tree, or enters the arm of the one given.
6912def zsh_branch(+entries: List<&2, Entry>, specs: List<&2, String>,
6913    +items: List<&2, Item>, +name: String, +words: List<&2, String>,
6914    arms: List<&2, String>, +ind: String) -> List<&2, String>:
6915  +state = String.join(name <> words, "-")
6916  +i2 = String.append(ind, "    ")
6917  +hstate = String.append(state, "-help")
6918  +label = zsh_label(name, words)
6919  List.concat(&2, String, [
6920    zsh_ends(words, ind),
6921    last_with(zsh_args(ind, List.append(&2, String, specs,
6922      [String.append("*:: :->", state)])), " && return"),
6923    [String.append(ind, "case $state in"),
6924      String.concat([ind, "  ", Sh.quote(state), ")"]),
6925      zsh_past_end(i2),
6926      String.append(i2, "if (( CURRENT == 1 )); then")],
6927    zsh_commands(entries, True{}, label, String.append(i2, "  ")),
6928    [String.append(i2, "else"), String.append(i2, "  case $words[1] in")],
6929    arms,
6930    [String.append(i2, "    'help')")],
6931    last_with(zsh_args(String.append(i2, "      "), ["(-h --help)-h[Print help]",
6932      "(-h --help)--help[Print help]", String.append("*:: :->", hstate)]),
6933      " && return"),
6934    [String.append(i2, "      case $state in"),
6935      String.concat([i2, "        ", Sh.quote(hstate), ")"]),
6936      zsh_past_end(String.append(i2, "          "))],
6937    last_with(zsh_help_tree(entries, label,
6938      zsh_help_arms(items, name, words, 1n,
6939        String.append(i2, "              ")),
6940      1n, String.append(i2, "          ")), " ;;"),
6941    [String.append(i2, "      esac ;;"), String.append(i2, "  esac"),
6942      String.append(i2, "fi ;;"), String.append(ind, "esac")],
6943  ])
6944
6945def zsh_body(entries: List<&2, Entry>, specs: List<&2, String>,
6946    +items: List<&2, Item>, +lists: List<&2, List<&2, String>>,
6947    name: String, words: List<&2, String>, arms: List<&2, String>,
6948    ind: String) -> List<&2, String>:
6949  match entries:
6950    case Nil{}:
6951      zsh_args(ind, List.append(&2, String, specs,
6952        zsh_slots(completing(arg_slots(items, 1n, lists)))))
6953    case head <> tail:
6954      zsh_branch(head <> tail, specs, items, name, words, arms, ind)
6955
6956# The body at ind of the command with items and globals at words, given
6957# the arms of its subcommands.
6958def zsh_cmd(+items: List<&2, Item>, +globals: List<&2, Item>,
6959    +lists: List<&2, List<&2, String>>, name: String,
6960    +words: List<&2, String>, arms: List<&2, String>, ind: String) ->
6961    List<&2, String>:
6962  zsh_body(sub_entries(items),
6963    List.append(&2, String,
6964      zsh_opts(List.append(&2, Opt, arg_opts(items, lists),
6965        arg_opts(globals, lists))),
6966      zsh_opts(builtin_opts(items, globals,
6967        List.is_empty(&2, String, words)))),
6968    items, lists, name, words, arms, ind)
6969
6970# The arms at ind for the visible subcommands in items, which inherit
6971# globals; their bodies are 2 deeper.
6972def zsh_arms(items: List<&2, Item>, +globals: List<&2, Item>,
6973    +lists: List<&2, List<&2, String>>, +name: String,
6974    +words: List<&2, String>, +ind: String) -> List<&2, String>:
6975  match items:
6976    case Nil{}: Nil{}
6977    case Sub{+sub, about, +children} <> tail:
6978      +here = List.append(&2, String, words, [sub])
6979      List.append(&2, String,
6980        Bool.pick(List<&2, String>, has_hidden(children), [],
6981          String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6982            last_with(zsh_cmd(children, globals, lists, name, here,
6983              zsh_arms(children, List.append(&2, Item, globals,
6984                global_items(children)), lists, name, here,
6985                String.append(ind, "          ")),
6986              String.append(ind, "  ")), " ;;")),
6987        zsh_arms(tail, globals, lists, name, words, ind))
6988    case head <> tail: zsh_arms(tail, globals, lists, name, words, ind)
6989
6990def zsh_lists(lists: List<&2, List<&2, String>>, +k: Nat) ->
6991    List<&2, String>:
6992  match lists:
6993    case Nil{}: Nil{}
6994    case values <> tail:
6995      String.concat(["v", Nat.show(k), "=(", sh_list(values, " "), ")"]) <>
6996        zsh_lists(tail, (k + 1n : Nat))
6997
6998def zsh_arrays(lists: List<&2, List<&2, String>>) -> List<&2, String>:
6999  match lists:
7000    case Nil{}: []
7001    case head <> tail:
7002      [String.append("  local -a ", String.join(zsh_lists(head <> tail, 1n),
7003        " "))]
7004
7005def zsh_script(+name: String, +items: List<&2, Item>) -> String:
7006  +lists = place_lists(all_places(items), [])
7007  +id = String.append("_", ident(name))
7008  text_lines(List.concat(&2, String, [
7009    [String.append("#compdef ", name), "", String.append(id, "() {"),
7010      "  local curcontext=$curcontext state line"],
7011    zsh_arrays(lists),
7012    zsh_cmd(items, [], lists, name, [],
7013      zsh_arms(items, global_items(items), lists, name, [], "          "),
7014      "  "),
7015    ["}",
7016      "",
7017      String.concat(["if [[ $funcstack[1] == ", Sh.quote(String.append("_",
7018        name)), " ]]; then"]),
7019      String.concat(["  ", id, " \"$@\""]),
7020      "else",
7021      String.concat(["  compdef ", id, " ", Sh.quote(name)]),
7022      "fi"],
7023  ]))
7024
7025# ---- fish
7026
7027# Words quoted for fish, apart by spaces.
7028def fish_list(words: List<&2, String>) -> String:
7029  match words:
7030    case Nil{}: ""
7031    case word <> Nil{}: Fish.quote(word)
7032    case word <> tail: String.concat([Fish.quote(word), " ", fish_list(tail)])
7033
7034def fish_pats(names: List<&2, String>, +pre: String) -> List<&2, String>:
7035  match names:
7036    case Nil{}: Nil{}
7037    case name <> tail: Fish.quote(String.append(pre, name)) <> fish_pats(tail, pre)
7038
7039# The arms of _dest for subcommands entries of the path at words, numbered
7040# pre.
7041def fish_dest_arms(entries: List<&2, Entry>, +pre: String,
7042    +words: List<&2, String>, +found: List<&2, Stop>) -> List<&2, String>:
7043  match entries:
7044    case Nil{}: Nil{}
7045    case Entry{name, names, about} <> tail:
7046      String.append("        case ", String.join(fish_pats(names, pre), " ")) <>
7047        String.append("            echo ", Nat.show(stop_number(found,
7048          List.append(&2, String, words, [name]), None{}))) <>
7049        fish_dest_arms(tail, pre, words, found)
7050
7051def fish_dests(stops: List<&2, Stop>, +found: List<&2, Stop>) ->
7052    List<&2, String>:
7053  match stops:
7054    case Nil{}: Nil{}
7055    case Stop{k, words, opts, builtins, subs, slots} <> tail:
7056      List.append(&2, String,
7057        fish_dest_arms(subs, String.append(Nat.show(k), " "), words, found),
7058        fish_dests(tail, found))
7059
7060def valued_names(opts: List<&2, Opt>) -> List<&2, String>:
7061  match opts:
7062    case Nil{}: Nil{}
7063    case Opt{names, OptionKind{many}, ph, comp, help} <> tail:
7064      List.append(&2, String, names, valued_names(tail))
7065    case head <> tail: valued_names(tail)
7066
7067def fish_valued_arm(k: Nat, names: List<&2, String>) -> List<&2, String>:
7068  match names:
7069    case Nil{}: []
7070    case head <> tail:
7071      [String.append("        case ", Nat.show(k)),
7072        String.append("            contains -- $argv[2] ",
7073          fish_list(head <> tail))]
7074
7075def fish_valued(stop: Stop) -> List<&2, String>:
7076  match stop:
7077    case Stop{k, words, opts, builtins, subs, slots}:
7078      fish_valued_arm(k, valued_names(opts))
7079
7080def slot_shape(found: Maybe<&2, Slot>) -> String:
7081  match found:
7082    case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
7083      String.concat([Nat.show(k), " ", Bool.pick(String, rest, "1", "0")])
7084    case None{}: "0 0"
7085
7086def fish_shape_arm(shown: Bool, k: Nat, has: Bool, found: Maybe<&2, Slot>) ->
7087    List<&2, String>:
7088  match shown:
7089    case True{}:
7090      [String.append("        case ", Nat.show(k)),
7091        String.concat(["            echo ", Bool.pick(String, has, "1", "0"),
7092          " ", slot_shape(found)])]
7093    case False{}: []
7094
7095def fish_shape(stop: Stop) -> List<&2, String>:
7096  match stop:
7097    case Stop{k, words, opts, builtins, subs, slots}:
7098      +has = Bool.not(List.is_empty(&2, Entry, subs))
7099      +found = variadic(slots)
7100      fish_shape_arm(Bool.or(has, Maybe.is_some(&2, Slot, found)), k, has,
7101        found)
7102
7103def fish_values(lists: List<&2, List<&2, String>>, +k: Nat) ->
7104    List<&2, String>:
7105  match lists:
7106    case Nil{}: Nil{}
7107    case values <> tail:
7108      String.append("        case ", Nat.show(k)) <>
7109        String.append("            printf '%s\\n' ", fish_list(values)) <>
7110        fish_values(tail, (k + 1n : Nat))
7111
7112def fish_values_fn(+id: String, lists: List<&2, List<&2, String>>) ->
7113    List<&2, String>:
7114  match lists:
7115    case Nil{}: []
7116    case head <> tail:
7117      List.concat(&2, String, [
7118        ["", "# Prints the values of the one_of list numbered $argv[1].",
7119          String.concat(["function ", id, "_values"]), "    switch $argv[1]"],
7120        fish_values(head <> tail, 1n),
7121        ["    end", "end"],
7122      ])
7123
7124def fish_flag(long: Bool, name: String) -> String:
7125  match long:
7126    case True{}: String.append("-l ", Fish.quote(String.drop(name, 2n)))
7127    case False{}: String.append("-s ", Fish.quote(String.drop(name, 1n)))
7128
7129def fish_flags(names: List<&2, String>) -> List<&2, String>:
7130  match names:
7131    case Nil{}: Nil{}
7132    case +name <> tail: fish_flag(long_name(name), name) <> fish_flags(tail)
7133
7134# What completes a value comp offers: words, files, directories or none.
7135def fish_words(+id: String, comp: Comp) -> String:
7136  match comp:
7137    case Words{list, values}:
7138      String.concat([" -a '(", id, "_values ", Nat.show(list), ")'"])
7139    case Files{}: " -F"
7140    case Dirs{}: " -a '(__fish_complete_directories)'"
7141    case Free{}: ""
7142
7143def fish_needs(comp: Comp) -> String:
7144  match comp:
7145    case Files{}: " -r"
7146    case _: " -x"
7147
7148# An option of kind needs its value (-r), and only a file name may be any
7149# word (-x elsewhere); a [=<V>] value is optional.
7150def fish_kind(kind: UsageKind, +id: String, +comp: Comp) -> String:
7151  match kind:
7152    case OptionKind{many}:
7153      String.append(fish_needs(comp), fish_words(id, comp))
7154    case BareOptionKind{}: fish_words(id, comp)
7155    case _: ""
7156
7157def fish_opt_lines(opts: List<&2, Opt>, +at: String, +id: String) ->
7158    List<&2, String>:
7159  match opts:
7160    case Nil{}: Nil{}
7161    case Opt{names, kind, ph, comp, help} <> tail:
7162      String.concat([at, " n v' ", String.join(fish_flags(names), " "),
7163        fish_kind(kind, id, comp), " -d ", Fish.quote(help)]) <>
7164        fish_opt_lines(tail, at, id)
7165
7166# The built-ins, help first, which also holds after help (h).
7167def fish_builtin_lines(opts: List<&2, Opt>, +at: String, tags: String) ->
7168    List<&2, String>:
7169  match opts:
7170    case Nil{}: Nil{}
7171    case Opt{names, kind, ph, comp, help} <> tail:
7172      String.concat([at, tags, "' ", String.join(fish_flags(names), " "),
7173        " -d ", Fish.quote(help)]) <> fish_builtin_lines(tail, at, " n")
7174
7175def fish_name_lines(names: List<&2, String>, +at: String, +about: String) ->
7176    List<&2, String>:
7177  match names:
7178    case Nil{}: Nil{}
7179    case name <> tail:
7180      String.concat([at, " n h' -a ", Fish.quote(name), " -d ", about]) <>
7181        fish_name_lines(tail, at, about)
7182
7183def fish_sub_lines(entries: List<&2, Entry>, +at: String) -> List<&2, String>:
7184  match entries:
7185    case Nil{}: Nil{}
7186    case Entry{name, names, about} <> tail:
7187      List.append(&2, String, fish_name_lines(names, at, Fish.quote(about)),
7188        fish_sub_lines(tail, at))
7189
7190def fish_slot_lines(slots: List<&2, Slot>, +at: String, +id: String) ->
7191    List<&2, String>:
7192  match slots:
7193    case Nil{}: Nil{}
7194    case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
7195      fish_slot_lines(tail, at, id)
7196    case Slot{k, False{}, required, many, rest, ph, Free{}} <> tail:
7197      fish_slot_lines(tail, at, id)
7198    case Slot{k, False{}, required, many, rest, ph, comp} <> tail:
7199      String.concat([at, " p", Nat.show(k), "'", fish_words(id, comp)]) <>
7200        fish_slot_lines(tail, at, id)
7201
7202def fish_ends(subs: List<&2, Entry>, +at: String, id: String,
7203    slots: List<&2, Slot>) -> List<&2, String>:
7204  match subs:
7205    case Nil{}: fish_slot_lines(completing(slots), at, id)
7206    case head <> tail:
7207      List.append(&2, String, fish_sub_lines(head <> tail, at),
7208        [String.append(at, " n' -a 'help' -d 'Print this message or the help of the given subcommand(s)'")])
7209
7210def fish_lines(stops: List<&2, Stop>, +pre: String, +id: String) ->
7211    List<&2, String>:
7212  match stops:
7213    case Nil{}: Nil{}
7214    case Stop{k, words, opts, builtins, subs, slots} <> tail:
7215      +at = String.append(pre, Nat.show(k))
7216      List.concat(&2, String, [
7217        fish_opt_lines(opts, at, id),
7218        fish_builtin_lines(builtins, at, " n h"),
7219        fish_ends(subs, at, id, slots),
7220        fish_lines(tail, pre, id),
7221      ])
7222
7223# The function every fish script shares, for function prefix id: it walks
7224# the words before the cursor as the bash engine does.
7225def fish_engine(+id: String) -> List<&2, String>:
7226  [
7227    "",
7228    "# Succeeds when the words before the cursor reach path $argv[1] and the next",
7229    "# word can be one of $argv[2..-1]: n, an option or subcommand; h, a",
7230    "# subcommand after help; v, an option's value; p<k>, positional slot k.",
7231    String.concat(["function ", id, "_is"]),
7232    "    # fish 4 deprecates -o, which fish 3 needs.",
7233    "    set -l words (commandline -xpc 2>/dev/null; or commandline -opc)",
7234    "    set -e words[1]",
7235    "    set -l path 0",
7236    String.concat(["    set -l shape (", id, "_shape 0 | string split ' ')"]),
7237    "    set -l want 0",
7238    "    set -l end 0",
7239    "    set -l help 0",
7240    "    set -l n 0",
7241    "    for word in $words",
7242    "        if test $want = 1",
7243    "            set want 0",
7244    "        else if test $end = 0; and test \"$word\" = --",
7245    "            set end 1",
7246    "        else if test $end = 0; and string match -q -r -- '^--.' $word",
7247    String.concat(["            if not string match -q -- '*=*' $word; and ", id, "_valued $path $word"]),
7248    "                set want 1",
7249    "            end",
7250    "        else if test $end = 0; and string match -q -r -- '^-.' $word",
7251    "            set -l chars (string split '' -- (string sub -s 2 -- $word))",
7252    "            for i in (seq (count $chars))",
7253    String.concat(["                if ", id, "_valued $path -$chars[$i]"]),
7254    "                    test $i = (count $chars); and set want 1",
7255    "                    break",
7256    "                end",
7257    "            end",
7258    "        else if test $shape[1] = 1",
7259    "            if test $end = 1",
7260    "                return 1",
7261    "            else if test $help = 0; and test \"$word\" = help",
7262    "                set help 1",
7263    "            else",
7264    String.concat(["                set path (", id, "_dest $path $word); or return 1"]),
7265    String.concat(["                set shape (", id, "_shape $path | string split ' ')"]),
7266    "            end",
7267    "        else",
7268    "            # n counts the slots filled. The variadic slot stays open, but a",
7269    "            # rest() value takes every later word, so nothing more completes.",
7270    "            set n (math $n + 1)",
7271    "            if test $n = $shape[2]; and test $shape[3] = 1",
7272    "                set end 1",
7273    "            else if test $n = $shape[2]",
7274    "                set n (math $n - 1)",
7275    "            end",
7276    "        end",
7277    "    end",
7278    "    test $path = $argv[1]; or return 1",
7279    "    set -l tags",
7280    "    if test $want = 1",
7281    "        set tags v",
7282    "    else",
7283    "        test $end = 0; and test $help = 0; and set -a tags n",
7284    "        test $end = 0; and test $help = 1; and set -a tags h",
7285    "        test $shape[1] = 0; and test $help = 0; and set -a tags p(math $n + 1)",
7286    "    end",
7287    "    for tag in $argv[2..-1]",
7288    "        contains -- $tag $tags; and return 0",
7289    "    end",
7290    "    return 1",
7291    "end",
7292  ]
7293
7294def fish_script(+name: String, +items: List<&2, Item>) -> String:
7295  +all = all_places(items)
7296  +lists = place_lists(all, [])
7297  +found = stops(all, 0n, lists)
7298  +id = String.append("__", ident(name))
7299  +cq = Fish.quote(name)
7300  text_lines(List.concat(&2, String, [
7301    ["# fish completion; needs fish 3.3 or later.",
7302      "",
7303      "# Prints the number of the path that subcommand $argv[2] of path $argv[1]",
7304      "# enters; paths are numbered in pre-order from 0, the root.",
7305      String.concat(["function ", id, "_dest"]),
7306      "    switch \"$argv[1] $argv[2]\""],
7307    fish_dests(found, found),
7308    ["        case '*'", "            return 1", "    end", "end", "",
7309      "# Succeeds when option $argv[2] takes the next word as its value at path",
7310      "# $argv[1].",
7311      String.concat(["function ", id, "_valued"]),
7312      "    switch $argv[1]"],
7313    bash_arms(~fish_valued, found),
7314    ["        case '*'", "            return 1", "    end", "end", "",
7315      "# Prints how path $argv[1] takes words: 1 if it has subcommands, else 0; the",
7316      "# slot of its variadic positional, or 0; 1 if that slot is rest(), else 0.",
7317      String.concat(["function ", id, "_shape"]),
7318      "    switch $argv[1]"],
7319    bash_arms(~fish_shape, found),
7320    ["        case '*'", "            echo 0 0 0", "    end", "end"],
7321    fish_values_fn(id, lists),
7322    fish_engine(id),
7323    ["",
7324      "# Option lines also hold at v, so that fish can read grouped short options",
7325      "# before a value, as in -vC DIR.",
7326      String.concat(["complete -c ", cq, " -f"])],
7327    fish_lines(found, String.concat(["complete -c ", cq, " -n '", id, "_is "]),
7328      id),
7329  ]))
7330
7331# ---- man pages
7332
7333def roff_bold(s: String) -> String:
7334  String.concat(["\\fB", roff_text(s), "\\fR"])
7335
7336def roff_bolds(names: List<&2, String>) -> List<&2, String>:
7337  match names:
7338    case Nil{}: Nil{}
7339    case name <> tail: roff_bold(String.trim_start(name)) <> roff_bolds(tail)
7340
7341def roff_texts(texts: List<&2, String>) -> List<&2, String>:
7342  match texts:
7343    case Nil{}: Nil{}
7344    case text <> tail: roff_text(text) <> roff_texts(tail)
7345
7346def spaces_only(s: String) -> Bool:
7347  match s:
7348    case SNil{}: True{}
7349    case SCon{Chr{32}, tail}: spaces_only(tail)
7350    case SCon{head, tail}: False{}
7351
7352def line_mark(blank: Bool, line: String) -> List<&2, Maybe<&2, String>>:
7353  match blank:
7354    case True{}: []
7355    case False{}: [Some{line}]
7356
7357# Each line, None for an empty one, which ends a paragraph; a line of only
7358# spaces is left out.
7359def para_marks(lines: List<&2, String>) -> List<&2, Maybe<&2, String>>:
7360  match lines:
7361    case Nil{}: Nil{}
7362    case SNil{} <> tail: None{} <> para_marks(tail)
7363    case +line <> tail:
7364      List.append(&2, Maybe<&2, String>, line_mark(spaces_only(line), line),
7365        para_marks(tail))
7366
7367# Marked lines escaped, after gap: nothing before the first, .br within a
7368# paragraph, sep between paragraphs.
7369def roff_marks(marks: List<&2, Maybe<&2, String>>, +sep: String,
7370    gap: Maybe<&2, String>) -> List<&2, String>:
7371  match marks gap:
7372    case Nil{} _: Nil{}
7373    case None{} <> tail None{}: roff_marks(tail, sep, None{})
7374    case None{} <> tail Some{g}: roff_marks(tail, sep, Some{sep})
7375    case Some{line} <> tail None{}:
7376      Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7377    case Some{line} <> tail Some{g}:
7378      g <> Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7379
7380def roff_paras(text: String, sep: String) -> List<&2, String>:
7381  roff_marks(para_marks(String.lines(text)), sep, None{})
7382
7383def ph_end(s: String) -> Nat:
7384  match s:
7385    case SNil{}: 0n
7386    case SCon{Chr{62}, tail}: 0n
7387    case SCon{head, tail}: 1n+ph_end(tail)
7388
7389# A placeholder's name in italics, from after its <.
7390def roff_ph(+s: String) -> String:
7391  +end = ph_end(s)
7392  String.concat([roff_text(String.take(s, end)), ">\\fR",
7393    roff_text(String.drop(s, (end + 1n : Nat)))])
7394
7395def roff_value(rest: String) -> String:
7396  match rest:
7397    case SCon{Chr{32}, SCon{Chr{60}, tail}}: String.append(" \\fI<", roff_ph(tail))
7398    case SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, tail}}}:
7399      String.append("[=\\fI<", roff_ph(tail))
7400    case SNil{}: ""
7401    case SCon{head, tail}: roff_text(SCon{head, tail})
7402
7403# Option names bold, the placeholder in italics.
7404def roff_option(+usage: String) -> String:
7405  +cut = value_cut(usage)
7406  String.append(String.join(roff_bolds(String.split(String.take(usage, cut),
7407    ',')), ", "), roff_value(String.drop(usage, cut)))
7408
7409# A positional in italics, apart from its repeat.
7410def roff_positional(+usage: String) -> String:
7411  +cut = value_cut(usage)
7412  String.concat(["\\fI", roff_text(String.take(usage, cut)), "\\fR",
7413    roff_text(String.drop(usage, cut))])
7414
7415def roff_entry(left: String, help: String, attrs: List<&2, Item>) ->
7416    List<&2, String>:
7417  ".TP" <> left <> roff_paras(right_text(True{}, help, attrs), ".IP")
7418
7419def roff_positional_entry(shown: Bool, usage: String, help: String,
7420    attrs: List<&2, Item>) -> List<&2, String>:
7421  match shown:
7422    case True{}: roff_entry(roff_positional(usage), help, attrs)
7423    case False{}: []
7424
7425def roff_positionals(items: List<&2, Item>) -> List<&2, String>:
7426  match items:
7427    case Nil{}: Nil{}
7428    case Arg{+usage, help, +attrs} <> tail:
7429      List.append(&2, String,
7430        roff_positional_entry(Bool.and(arg_pos(usage),
7431          Bool.not(has_hidden(attrs))), usage, help, attrs),
7432        roff_positionals(tail))
7433    case head <> tail: roff_positionals(tail)
7434
7435def roff_option_entry(shown: Bool, usage: String, help: String,
7436    attrs: List<&2, Item>) -> List<&2, String>:
7437  match shown:
7438    case True{}: roff_entry(roff_option(usage), help, attrs)
7439    case False{}: []
7440
7441# The visible options under a heading, "" for OPTIONS.
7442def roff_options(items: List<&2, Item>, +heading: String) -> List<&2, String>:
7443  match items:
7444    case Nil{}: Nil{}
7445    case Arg{+usage, help, +attrs} <> tail:
7446      List.append(&2, String,
7447        roff_option_entry(Bool.and(Bool.not(arg_pos(usage)),
7448          Bool.and(Bool.not(has_hidden(attrs)),
7449            String.eq(find_heading(attrs), heading))), usage, help, attrs),
7450        roff_options(tail, heading))
7451    case head <> tail: roff_options(tail, heading)
7452
7453def roff_builtins(opts: List<&2, Opt>) -> List<&2, String>:
7454  match opts:
7455    case Nil{}: Nil{}
7456    case Opt{names, kind, ph, comp, help} <> tail:
7457      ".TP" <> String.join(roff_bolds(names), ", ") <> help <>
7458        roff_builtins(tail)
7459
7460def roff_headings(titles: List<&2, String>, +args: List<&2, Item>) ->
7461    List<&2, String>:
7462  match titles:
7463    case Nil{}: Nil{}
7464    case +title <> tail:
7465      String.concat([".SS \"", roff_text(title), "\""]) <>
7466        List.append(&2, String, roff_options(args, title),
7467          roff_headings(tail, args))
7468
7469def roff_command(hidden: Bool, +page: String, name: String, about: String,
7470    children: List<&2, Item>) -> List<&2, String>:
7471  match hidden:
7472    case True{}: []
7473    case False{}:
7474      [".TP", String.append(roff_bold(String.concat([page, "-", name])), "(1)"),
7475        Roff.esc(join_filled([one_line(about),
7476          tag("aliases", find_aliases(children))]))]
7477
7478def roff_commands(items: List<&2, Item>, +page: String) -> List<&2, String>:
7479  match items:
7480    case Nil{}: Nil{}
7481    case Sub{name, about, +children} <> tail:
7482      List.append(&2, String,
7483        roff_command(has_hidden(children), page, name, about, children),
7484        roff_commands(tail, page))
7485    case head <> tail: roff_commands(tail, page)
7486
7487def roff_arg(pos: Bool, +usage: String, attrs: List<&2, Item>) -> String:
7488  match pos:
7489    case True{}: roff_positional(usage)
7490    case False{}: roff_option(arg_display(arg_info(usage, attrs)))
7491
7492def roff_env_names(names: List<&2, String>) -> List<&2, String>:
7493  match names:
7494    case Nil{}: Nil{}
7495    case name <> tail: roff_bold(name) <> roff_env_names(tail)
7496
7497# How a variable after the first is read, if there is one.
7498def roff_env_order(rest: List<&2, String>) -> String:
7499  match rest:
7500    case Nil{}: ""
7501    case head <> tail: "; the first that is set wins"
7502
7503def roff_env(hidden: Bool, names: List<&2, String>, +usage: String,
7504    attrs: List<&2, Item>) -> List<&2, String>:
7505  match hidden names:
7506    case False{} name <> +tail:
7507      [".TP", String.join(roff_env_names(name <> tail), ", "),
7508        String.concat(["Used for ", roff_arg(arg_pos(usage), usage, attrs),
7509          " when it is not given", roff_env_order(tail)])]
7510    case _ _: []
7511
7512def roff_envs(items: List<&2, Item>) -> List<&2, String>:
7513  match items:
7514    case Nil{}: Nil{}
7515    case Arg{usage, help, +attrs} <> tail:
7516      List.append(&2, String,
7517        roff_env(has_hidden(attrs), find_envs(attrs), usage, attrs),
7518        roff_envs(tail))
7519    case head <> tail: roff_envs(tail)
7520
7521def roff_examples(items: List<&2, Item>, before: List<&2, String>) ->
7522    List<&2, String>:
7523  match items:
7524    case Nil{}: Nil{}
7525    case Example{line, what} <> tail:
7526      List.concat(&2, String, [
7527        before,
7528        [Roff.esc(String.append(what, ":")), ".IP", ".nf",
7529          Roff.esc(String.append("$ ", line)), ".fi"],
7530        roff_examples(tail, [".PP"]),
7531      ])
7532    case head <> tail: roff_examples(tail, before)
7533
7534def sub_pages(items: List<&2, Item>, +page: String) -> List<&2, String>:
7535  match items:
7536    case Nil{}: Nil{}
7537    case Sub{name, about, +children} <> tail:
7538      +rest = sub_pages(tail, page)
7539      Bool.pick(List<&2, String>, has_hidden(children), rest,
7540        String.concat([page, "-", name]) <> rest)
7541    case head <> tail: sub_pages(tail, page)
7542
7543def init_words(words: List<&2, String>) -> List<&2, String>:
7544  match words:
7545    case Nil{}: Nil{}
7546    case word <> Nil{}: Nil{}
7547    case word <> tail: word <> init_words(tail)
7548
7549# The parent's page, none at the root.
7550def parent_pages(words: List<&2, String>) -> List<&2, String>:
7551  match words:
7552    case Nil{}: []
7553    case word <> Nil{}: []
7554    case word <> tail: [String.join(word <> init_words(tail), "-")]
7555
7556def roff_refs(pages: List<&2, String>) -> List<&2, String>:
7557  match pages:
7558    case Nil{}: Nil{}
7559    case page <> tail: String.append(roff_bold(page), "(1)") <> roff_refs(tail)
7560
7561def roff_see(pages: List<&2, String>) -> List<&2, String>:
7562  match pages:
7563    case Nil{}: []
7564    case head <> tail: [String.join(roff_refs(head <> tail), ", ")]
7565
7566def roff_synopsis(texts: List<&2, String>) -> String:
7567  match texts:
7568    case Nil{}: ""
7569    case path <> tail: String.join(roff_bold(path) <> roff_texts(tail), " ")
7570
7571def roff_section(title: String, lines: List<&2, String>) -> List<&2, String>:
7572  match lines:
7573    case Nil{}: []
7574    case head <> tail: String.append(".SH ", title) <> head <> tail
7575
7576def roff_view(view: View, title: String) -> String:
7577  match view:
7578    case View{+path, +about, +items, +globals, root, stuck}:
7579      +words = String.split(path, ' ')
7580      +page = String.join(words, "-")
7581      +args = List.append(&2, Item, items, globals)
7582      text_lines(List.concat(&2, String, [
7583        [String.concat([".TH \"", roff_text(String.to_upper(page)),
7584          "\" \"1\" \"\" \"", roff_text(title), "\" \"User Commands\""]),
7585          ".SH NAME",
7586          Roff.esc(String.concat([page, " - ", one_line(about)])),
7587          ".SH SYNOPSIS",
7588          roff_synopsis(segment_texts(usage_elems(path, items, globals)))],
7589        roff_section("DESCRIPTION", roff_paras(about, ".PP")),
7590        roff_section("ARGUMENTS", roff_positionals(items)),
7591        [".SH OPTIONS"],
7592        roff_options(args, ""),
7593        roff_builtins(builtin_opts(items, globals, root)),
7594        roff_headings(heading_titles(args, []), args),
7595        roff_section("COMMANDS", roff_commands(items, page)),
7596        roff_section("ENVIRONMENT", roff_envs(args)),
7597        [".SH EXIT STATUS", ".TP", "\\fB0\\fR", "Success", ".TP", "\\fB1\\fR",
7598          "Failure", ".TP", "\\fB2\\fR", "Usage error", ".TP", "\\fB70\\fR",
7599          "Internal software error"],
7600        roff_section("EXAMPLES", roff_examples(items, [])),
7601        roff_section("NOTES",
7602          roff_paras(String.join(after_texts(items), "\n\n"), ".PP")),
7603        roff_section("SEE ALSO", roff_see(List.append(&2, String,
7604          parent_pages(words), sub_pages(items, page)))),
7605      ]))
7606
7607def find_version(items: List<&2, Item>) -> String:
7608  match items:
7609    case Nil{}: ""
7610    case Version{value} <> tail: value
7611    case head <> tail: find_version(tail)
7612
7613# The man page of the command at the longest valid prefix of path (§11).
7614def roff(c: Cmd, path: List<&2, String>) -> String:
7615  match c:
7616    case Cmd{+name, about, +items}:
7617      roff_view(view_walk(path, View{name, about, items, [], True{}, False{}}),
7618        join_filled([name, find_version(items)]))
7619
7620# The completion script for shell s (§11).
7621def script(c: Cmd, s: Shell) -> String:
7622  match c s:
7623    case Cmd{name, about, items} Bash{}: bash_script(name, items)
7624    case Cmd{name, about, items} Zsh{}: zsh_script(name, items)
7625    case Cmd{name, about, items} Fish{}: fish_script(name, items)
7626
7627# ---- Outcome (§8.7, V1-V6) ----
7628
7629type Outcome is Data:
7630  Shown{code: U32, out: String, err: String}
7631  Matched{m: Matches}
7632
7633# The subcommand at path, from found on.
7634def sub_at(path: List<&2, String>, items: List<&2, Item>,
7635    found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
7636  match path:
7637    case Nil{}: found
7638    case name <> tail:
7639      +next = find_sub(items, name)
7640      sub_at(tail, sub_items(next), next)
7641
7642def builtin_name(found: Maybe<&2, SubFound>) -> Maybe<&2, String>:
7643  match found:
7644    case Some{SubFound{name, items}}:
7645      Bool.pick(Maybe<&2, String>, has_builtin(items), Some{name}, None{})
7646    case None{}: None{}
7647
7648def field_values(f: Field) -> List<&2, String>:
7649  match f:
7650    case Field{id, values, source}: values
7651
7652# The shell completions was given, which one_of limits to these three.
7653def shell_named(+name: String) -> Shell:
7654  Bool.pick(Shell, String.eq(name, "zsh"), Zsh{},
7655    Bool.pick(Shell, String.eq(name, "fish"), Fish{}, Bash{}))
7656
7657def builtin_page(man: Bool, c: Cmd, fields: List<&2, Field>) -> String:
7658  match man:
7659    case True{}: roff(c, field_values(field_find(fields, "command")))
7660    case False{}:
7661      script(c, shell_named(head_part(field_values(field_find(fields,
7662        "shell")))))
7663
7664# V6: the completions built-in prints its script and man its page, plain;
7665# any other path is left to decode.
7666def outcome_accepted(builtin: Maybe<&2, String>, c: Cmd,
7667    path: List<&2, String>, fields: List<&2, Field>,
7668    external: Maybe<&2, ExternalArgs>) -> Outcome:
7669  match builtin:
7670    case Some{name}: Shown{0, builtin_page(String.eq(name, "man"), c, fields), ""}
7671    case None{}: Matched{Matches{c, path, fields, external}}
7672
7673def outcome_parse(result: ParseResult, +c: Cmd, h: Host,
7674    +argv: List<&2, String>) -> Outcome:
7675  match result:
7676    case Accepted{+path, fields, external}:
7677      outcome_accepted(builtin_name(sub_at(path, cmd_items(c), None{})), c,
7678        path, fields, external)
7679    case HelpRequested{path, long}:
7680      Shown{0, help_out(term(c, h, argv), view_of(c, path), long), ""}
7681    case VersionRequested{}:
7682      Shown{0, String.concat([cmd_name(c), " ", find_version(cmd_items(c)),
7683        "\n"]), ""}
7684    case SummaryRequired{}:
7685      Shown{2, "", summary_err(term(c, h, argv), view_of(c, []))}
7686    case Rejected{path, fault}:
7687      Shown{2, "", fault_err(term(c, h, argv), fault, c, path, argv)}
7688
7689def outcome_lint(found: List<&2, Problem>, +c: Cmd, +h: Host,
7690    +argv: List<&2, String>) -> Outcome:
7691  match found:
7692    case Nil{}: outcome_parse(parse(c, h, argv), c, h, argv)
7693    case p <> tail:
7694      Shown{70, "", String.append("bug: invalid command-line definition\n",
7695        problem_lines(p <> tail))}
7696
7697# eval through V6: a lint failure, help, version, the V3 summary, an error
7698# or a built-in's output is Shown; Matched is a parse left to decode.
7699def outcome(+c: Cmd, h: Host, argv: List<&2, String>) -> Outcome:
7700  outcome_lint(problems(c), c, h, argv)
7701
7702# A Cmd that passed lint, from linted: eval_linted skips the lint eval runs.
7703type Linted is Data:
7704  Linted{cmd: Cmd}
7705
7706def linted_found(found: List<&2, Problem>, c: Cmd) ->
7707    Result<&2, &2, List<&2, Problem>, Linted>:
7708  match found:
7709    case Nil{}: Done{Linted{c}}
7710    case p <> tail: Fail{p <> tail}
7711
7712# Lint c once: a Linted, or lint's problems in the order problems lists.
7713def linted(+c: Cmd) -> Result<&2, &2, List<&2, Problem>, Linted>:
7714  linted_found(problems(c), c)
7715
7716# outcome on a Linted's spec, without the lint.
7717def outcome_linted(l: Linted, +h: Host, +argv: List<&2, String>) -> Outcome:
7718  match l:
7719    case Linted{+c}: outcome_parse(parse(c, h, argv), c, h, argv)
7720
7721# ---- Eval (§6.6, §7.2, §7.3, V7) ----
7722
7723type Step<-A: Type> is Type:
7724  Go{value: A, term: Term}
7725  Quit{code: U32, out: String, err: String}
7726
7727# E12: a decoder's Usage message as an error at the matched scope, printed
7728# as given, in stderr's colors.
7729def usage_err(t: Term, msg: String, view: View) -> String:
7730  match t view:
7731    case Term{out_color, err_color, width}
7732        View{here, about, items, globals, root, stuck}:
7733      emit(err_color, error_page([plain(msg)], [],
7734        usage_block(usage_elems(here, items, globals), page_width(width))))
7735
7736def faulted(-A: Type, f: Fault, +cmd: Cmd, path: List<&2, String>, h: Host,
7737    argv: List<&2, String>) -> Step<A>:
7738  match f:
7739    case Bug{msg}: Quit{70, "", String.concat(["bug: ", msg, "\n"])}
7740    case Usage{msg}:
7741      Quit{2, "", usage_err(term(cmd, h, argv), msg, view_of(cmd, path))}
7742
7743# V7: Done is Go with term's decisions; a fault exits 70 or 2.
7744def decoded(-A: Type, d: Dec(A), cmd: Cmd, path: List<&2, String>, h: Host,
7745    argv: List<&2, String>) -> Step<A>:
7746  match d:
7747    case Done{a}: Go{a, term(cmd, h, argv)}
7748    case Fail{f}: faulted(A, f, cmd, path, h, argv)
7749
7750def eval_step(-A: Type, decode: @+m: Matches -> Dec(A), o: Outcome, h: Host,
7751    argv: List<&2, String>) -> Step<A>:
7752  match o:
7753    case Shown{code, out, err}: Quit{code, out, err}
7754    case Matched{Matches{+cmd, +path, fields, external}}:
7755      decoded(A, decode(Matches{cmd, path, fields, external}), cmd, path, h,
7756        argv)
7757
7758# eval with the decoder as a value, so a law can quantify over decoders.
7759def eval_with(-A: Type, decode: @+m: Matches -> Dec(A), c: Cmd, +h: Host,
7760    +argv: List<&2, String>) -> Step<A>:
7761  eval_step(A, decode, outcome(c, h, argv), h, argv)
7762
7763# The whole CLI as a function. It must stay a direct forwarding definition:
7764# P10 and P11 are proved of eval_with.
7765def eval(~A: Type, ~decode: @+m: Matches -> Dec(A), c: Cmd, h: Host,
7766    argv: List<&2, String>) -> Step<A>:
7767  eval_with(A, decode, c, h, argv)
7768
7769# eval_linted with the decoder as a value.
7770def eval_linted_with(-A: Type, decode: @+m: Matches -> Dec(A), l: Linted,
7771    +h: Host, +argv: List<&2, String>) -> Step<A>:
7772  eval_step(A, decode, outcome_linted(l, h, argv), h, argv)
7773
7774# eval on a Linted, without linting its spec again. It must stay a direct
7775# forwarding definition: P16, p10_linted and p11_linted are proved of
7776# eval_linted_with.
7777def eval_linted(~A: Type, ~decode: @+m: Matches -> Dec(A), l: Linted,
7778    h: Host, argv: List<&2, String>) -> Step<A>:
7779  eval_linted_with(A, decode, l, h, argv)
7780
7781# Each arg's env name in pre-order: an arg's own, then its subcommands'.
7782def env_names(items: List<&2, Item>) -> List<&2, String>:
7783  match items:
7784    case Nil{}: Nil{}
7785    case Arg{usage, help, attrs} <> tail:
7786      List.append(&2, String, find_envs(attrs), env_names(tail))
7787    case Sub{name, about, children} <> tail:
7788      List.append(&2, String, env_names(children), env_names(tail))
7789    case head <> tail: env_names(tail)
7790
7791# kept, then each name not yet in it.
7792def distinct(names: List<&2, String>, kept: List<&2, String>) ->
7793    List<&2, String>:
7794  match names:
7795    case Nil{}: kept
7796    case name <> tail: distinct(tail, append_unique(kept, name))
7797
7798def vars(c: Cmd) -> List<&2, String>:
7799  match c:
7800    case Cmd{name, about, items}:
7801      distinct(List.append(&2, String, env_names(items),
7802        ["NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE", "TERM", "COLUMNS"]), [])
7803
7804# COLUMNS sets the width when as_nat reads it and it is at least 1. digits
7805# is bound because a temporary argument makes String.is_empty take its
7806# string, adding a take to each caller that reuses one (valid_nat).
7807def columns_ok(+value: String) -> Bool:
7808  digits = Nat.read.trim(value)
7809  Bool.and(valid_nat(value), Bool.not(String.is_empty(digits)))
7810
7811def columns_valid(ok: Bool, value: String) -> Maybe<&2, Nat>:
7812  match ok:
7813    case True{}: Some{nat_of(value)}
7814    case False{}: None{}
7815
7816def columns_width(found: Maybe<&2, String>) -> Maybe<&2, Nat>:
7817  match found:
7818    case None{}: None{}
7819    case Some{+value}: columns_valid(columns_ok(value), value)
7820
7821# stdout's columns if it is a TTY, else stderr's if it is one, else 0.
7822def tty_columns(out_tty: Bool, err_tty: Bool, out_cols: U32,
7823    err_cols: U32) -> Nat:
7824  match out_tty err_tty:
7825    case True{} _: U32.to_nat(out_cols)
7826    case False{} True{}: U32.to_nat(err_cols)
7827    case False{} False{}: 0n
7828
7829# first, unless it is 0.
7830def nonzero(first: Nat, then: Nat) -> Nat:
7831  match first:
7832    case 0n: then
7833    case 1n+p: 1n+p
7834
7835def width_or(found: Maybe<&2, Nat>, then: Nat) -> Nat:
7836  match found:
7837    case None{}: then
7838    case Some{width}: width
7839
7840# §7.3: the host from Run.host's readings; the width is COLUMNS, else the
7841# TTY columns, else 80.
7842def probe(+vars: List<&2, Var>, +out_tty: Bool, +err_tty: Bool,
7843    out_cols: U32, err_cols: U32) -> Host:
7844  Host{width_or(columns_width(var_value(vars, "COLUMNS")),
7845    nonzero(tty_columns(out_tty, err_tty, out_cols, err_cols), 80n)),
7846    vars, out_tty, err_tty}
7847
7848def act_code(fits: Bool, n: U32) -> (U32 & String):
7849  match fits:
7850    case True{}: (n, "")
7851    case False{}:
7852      (70, String.concat(["bug: exit code ", U32.show(n),
7853        " is out of range 0..255\n"]))
7854
7855# §7.2: act's code, or 70 with a stderr line when it exceeds 255.
7856def act_exit(+n: U32) -> (U32 & String):
7857  act_code(U32.is_le(n, 255), n)
7858
7859# ---- Pure runs (§7.2) ----
7860
7861# What a run prints and how it ends: its exit status, its stdout text and
7862# its stderr text.
7863type Ran is Data:
7864  Ran{code: U32, out: String, err: String}
7865
7866def owed_end(newline: Bool) -> String:
7867  match newline:
7868    case True{}: ""
7869    case False{}: "\n"
7870
7871def owed_after(t: String, c: Char) -> String:
7872  match t:
7873    case SNil{}: owed_end(Char.is_eq(c, '\n'))
7874    case SCon{d, u}: owed_after(u, d)
7875
7876# "\n" unless s is empty or ends with one: what IO.print_err adds to s.
7877def owed(s: String) -> String:
7878  match s:
7879    case SNil{}: ""
7880    case SCon{c, t}: owed_after(t, c)
7881
7882def ran_exit(e: (U32 & String), out: String, +err: String) -> Ran:
7883  match e:
7884    case (code, bug): Ran{code, out, String.concat([err, owed(err), bug])}
7885
7886# r with act_exit's mapping: a code above 255 becomes 70, with the bug line
7887# after r's stderr.
7888def ran_act(r: Ran) -> Ran:
7889  match r:
7890    case Ran{code, out, err}: ran_exit(act_exit(code), out, err)
7891
7892def ran_step(~A: Type, ~plan: @+t: Term -> A -> Ran, s: Step<A>) -> Ran:
7893  match s:
7894    case Quit{code, out, err}: Ran{code, out, err}
7895    case Go{a, t}: ran_act(plan(t, a))
7896
7897# §7.2: what Run.planned writes and exits with, given the host and argv:
7898# Quit's code and text, or plan's run on the value and terminal, mapped by
7899# ran_act. plan builds its output whole, so an action that streams or does
7900# other IO is an act for Run.main instead.
7901def invoke(~A: Type, ~decode: @+m: Matches -> Dec(A),
7902    ~plan: @+t: Term -> A -> Ran, c: Cmd, h: Host,
7903    argv: List<&2, String>) -> Ran:
7904  ran_step(~A, ~plan, eval(~A, ~decode, c, h, argv))
7905
7906# ---- Argument bytes (§7.2) ----
7907
7908# What a byte starts: no UTF-8 sequence (a continuation byte, C0, C1, F5
7909# to FF, or a value past 255), or one with more continuation bytes, the
7910# code point bits the byte carries, and the least code point a sequence
7911# that long may hold, below which it is overlong.
7912type Utf8Lead is Data:
7913  NotLead{}
7914  Utf8Lead{more: Nat, bits: U32, least: U32}
7915
7916def utf8_lead(+b: U32) -> Utf8Lead:
7917  Bool.pick(Utf8Lead, U32.is_lt(b, 194), NotLead{},
7918    Bool.pick(Utf8Lead, U32.is_lt(b, 224), Utf8Lead{1n, (b - 192 : U32), 128},
7919      Bool.pick(Utf8Lead, U32.is_lt(b, 240),
7920        Utf8Lead{2n, (b - 224 : U32), 2048},
7921        Bool.pick(Utf8Lead, U32.is_lt(b, 245),
7922          Utf8Lead{3n, (b - 240 : U32), 65536}, NotLead{}))))
7923
7924def utf8_next(ok: Bool, found: Maybe<&2, U32>) -> Maybe<&2, U32>:
7925  match ok:
7926    case True{}: found
7927    case False{}: None{}
7928
7929# The code point acc continues to through the first more bytes of rest,
7930# each a continuation byte (80..BF); None if rest ends first or one is not.
7931def utf8_tail(rest: List<&2, U32>, more: Nat, acc: U32) -> Maybe<&2, U32>:
7932  match rest more:
7933    case Nil{} 0n: Some{acc}
7934    case Nil{} 1n+m: None{}
7935    case Con{b, t} 0n: Some{acc}
7936    case Con{+b, t} 1n+m:
7937      utf8_next(U32.is_lt((b - 128 : U32), 64),
7938        utf8_tail(t, m, (acc * 64 + (b - 128) : U32)))
7939
7940# A decoded code point, if it is a Unicode scalar at least least.
7941def utf8_scalar(found: Maybe<&2, U32>, least: U32) -> Maybe<&2, U32>:
7942  match found:
7943    case None{}: None{}
7944    case Some{+x}:
7945      utf8_next(Bool.and(Bool.and(U32.is_ge(x, least),
7946        U32.is_le(x, 1114111)), U32.is_ge((x - 55296 : U32), 2048)), Some{x})
7947
7948# U+DC00 plus byte b, PEP 383's escape for a byte that starts no
7949# well-formed UTF-8 sequence; U+FFFD for a value past 255, no byte.
7950def byte_escape(+b: U32) -> Char:
7951  Chr{Bool.pick(U32, U32.is_le(b, 255), (b + 56320 : U32), 65533)}
7952
7953# A char decoded from argument bytes, and how many bytes after its first
7954# it took.
7955type ArgChar is Data:
7956  ArgChar{c: Char, more: Nat}
7957
7958def arg_found(found: Maybe<&2, U32>, more: Nat, b: U32) -> ArgChar:
7959  match found:
7960    case None{}: ArgChar{byte_escape(b), 0n}
7961    case Some{x}: ArgChar{Chr{x}, more}
7962
7963def arg_seq(lead: Utf8Lead, b: U32, rest: List<&2, U32>) -> ArgChar:
7964  match lead:
7965    case NotLead{}: ArgChar{byte_escape(b), 0n}
7966    case Utf8Lead{+more, bits, least}:
7967      arg_found(utf8_scalar(utf8_tail(rest, more, bits), least), more, b)
7968
7969def arg_char_by(ascii: Bool, +b: U32, rest: List<&2, U32>) -> ArgChar:
7970  match ascii:
7971    case True{}: ArgChar{Chr{b}, 0n}
7972    case False{}: arg_seq(utf8_lead(b), b, rest)
7973
7974# The char that byte b and the bytes after it, rest, start.
7975def arg_char(+b: U32, rest: List<&2, U32>) -> ArgChar:
7976  arg_char_by(U32.is_lt(b, 128), b, rest)
7977
7978# step's char, then bytes as text once step's other bytes are skipped.
7979def arg_text_go(bytes: List<&2, U32>, step: ArgChar) -> String:
7980  match bytes step:
7981    case Nil{} ArgChar{c, more}: SCon{c, SNil{}}
7982    case Con{b, rest} ArgChar{c, 1n+m}: arg_text_go(rest, ArgChar{c, m})
7983    case Con{b, +rest} ArgChar{c, 0n}:
7984      SCon{c, arg_text_go(rest, arg_char(b, rest))}
7985
7986# A raw argument as text: each well-formed UTF-8 sequence as its char, and
7987# each byte that starts none as U+DC00 plus the byte (PEP 383), decoding on
7988# from the next byte. Run.main reads argv through it.
7989def arg_text(bytes: List<&2, U32>) -> String:
7990  match bytes:
7991    case Nil{}: SNil{}
7992    case Con{b, +rest}: arg_text_go(rest, arg_char(b, rest))
7993
7994def cont_byte(x: U32) -> U32:
7995  (128 + x % 64 : U32)
7996
7997# Scalar x's UTF-8, for x from 80: 2 bytes below 800, 3 below 10000, else 4.
7998def utf8_bytes(two: Bool, three: Bool, +x: U32) -> List<&2, U32>:
7999  match two three:
8000    case True{} _: [(192 + x / 64 : U32), cont_byte(x)]
8001    case False{} True{}:
8002      [(224 + x / 4096 : U32), cont_byte((x / 64 : U32)), cont_byte(x)]
8003    case False{} False{}:
8004      [(240 + x / 262144 : U32), cont_byte((x / 4096 : U32)),
8005        cont_byte((x / 64 : U32)), cont_byte(x)]
8006
8007def code_bytes(ascii: Bool, escape: Bool, scalar: Bool, +x: U32) ->
8008    List<&2, U32>:
8009  match ascii escape scalar:
8010    case True{} _ _: [x]
8011    case False{} True{} _: [(x - 56320 : U32)]
8012    case False{} False{} True{}:
8013      utf8_bytes(U32.is_lt(x, 2048), U32.is_lt(x, 65536), x)
8014    case False{} False{} False{}: [239, 191, 189]
8015
8016# The bytes c stands for: an escape's byte for U+DC80..U+DCFF, a Unicode
8017# scalar's UTF-8, and U+FFFD's for any other surrogate or a value past
8018# U+10FFFF, as Bend's writer prints them.
8019def char_bytes(c: Char) -> List<&2, U32>:
8020  match c:
8021    case Chr{+x}:
8022      code_bytes(U32.is_lt(x, 128), U32.is_lt((x - 56448 : U32), 128),
8023        Bool.and(U32.is_le(x, 1114111), U32.is_ge((x - 55296 : U32), 2048)),
8024        x)
8025
8026# The raw argument arg_text decoded to s: open a file by it with
8027# File.open_bytes.
8028def arg_bytes(s: String) -> List<&2, U32>:
8029  match s:
8030    case SNil{}: []
8031    case SCon{c, t}: List.append(&2, U32, char_bytes(c), arg_bytes(t))