cli.bend

cli@0.1.0.0 · 7,654 lines · 0 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# A scope's own args answer before its globals, as in find_option.
2628def find_option_scope(scope: Scope, query: String) ->
2629    Maybe<&2, OptFound>:
2630  match scope:
2631    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2632        external, needs_sub, version, digit_short, inherit}}:
2633      find_key(keys, query, None{})
2634
2635# Whether items alias query; after a hit it walks no further.
2636def has_alias(items: List<&2, Item>, +query: String, found: Bool) -> Bool:
2637  match items found:
2638    case _ True{}: True{}
2639    case Nil{} False{}: False{}
2640    case Alias{name} <> tail False{}:
2641      has_alias(tail, query, same_text(name, query))
2642    case head <> tail False{}: has_alias(tail, query, False{})
2643
2644def alias_hit(aliased: Bool, name: String, children: List<&2, Item>) ->
2645    Maybe<&2, SubFound>:
2646  match aliased:
2647    case True{}: Some{SubFound{name, children}}
2648    case False{}: None{}
2649
2650def sub_hit(same: Bool, name: String, +children: List<&2, Item>,
2651    query: String) -> Maybe<&2, SubFound>:
2652  match same:
2653    case True{}: Some{SubFound{name, children}}
2654    case False{}:
2655      alias_hit(has_alias(children, query, False{}), name, children)
2656
2657def sub_found(item: Item, +query: String) -> Maybe<&2, SubFound>:
2658  match item:
2659    case Sub{+name, about, children}:
2660      sub_hit(same_text(query, name), name, children, query)
2661    case _: None{}
2662
2663# The first Sub named or aliased query; after a hit it walks no further.
2664def find_sub_go(items: List<&2, Item>, +query: String,
2665    found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
2666  match items found:
2667    case _ Some{hit}: Some{hit}
2668    case Nil{} None{}: None{}
2669    case item <> tail None{}: find_sub_go(tail, query, sub_found(item, query))
2670
2671def find_sub(items: List<&2, Item>, +query: String) ->
2672    Maybe<&2, SubFound>:
2673  find_sub_go(items, query, None{})
2674
2675def positional_items(items: List<&2, Item>) -> List<&2, Item>:
2676  match items:
2677    case Nil{}: Nil{}
2678    case Arg{+usage, +help, +attrs} <> tail:
2679      +rest = positional_items(tail)
2680      Bool.pick(List<&2, Item>,
2681        usage_kind(~kind_pos, parse_usage(usage)),
2682        Arg{usage, help, attrs} <> rest, rest)
2683    case head <> tail: positional_items(tail)
2684
2685def has_variadic_pos(items: List<&2, Item>) -> Bool:
2686  match items:
2687    case Nil{}: False{}
2688    case Arg{usage, help, attrs} <> tail:
2689      Bool.or(usage_kind(~kind_variadic_pos, parse_usage(usage)),
2690        has_variadic_pos(tail))
2691    case head <> tail: has_variadic_pos(tail)
2692
2693def positional_can_take(items: List<&2, Item>, count: Nat) -> Bool:
2694  +slots = positional_items(items)
2695  Bool.or(Nat.is_lt(count, List.length(&2, Item, slots)),
2696    has_variadic_pos(slots))
2697
2698def next_slot(slots: List<&2, ArgInfo>, +count: Nat) ->
2699    Maybe<&2, ArgInfo>:
2700  match slots:
2701    case Nil{}: None{}
2702    case +info <> tail:
2703      Bool.pick(Maybe<&2, ArgInfo>,
2704        Bool.or(Nat.is_eq(count, 0n), kind_variadic_pos(info_kind(info))),
2705        Some{info}, next_slot(tail, Nat.sub(count, 1n)))
2706
2707def scope_next_slot(scope: Scope, count: Nat) -> Maybe<&2, ArgInfo>:
2708  match scope:
2709    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2710        external, needs_sub, version, digit_short, inherit}}:
2711      next_slot(slots, count)
2712
2713# name as a key of info; only info's --no- long clears the flag, as in
2714# option_matches.
2715def opt_key(+name: String, info: ArgInfo) -> OptKey:
2716  match info:
2717    case ArgInfo{id, usage, short, +long, +kind, attrs}:
2718      OptKey{name, OptFound{ArgInfo{id, usage, short, long, kind, attrs},
2719        Bool.not(Bool.and(kind_negatable(kind),
2720          Bool.and(Bool.not(String.is_empty(long)),
2721            String.eq(name, negated_long(long)))))}}
2722
2723def name_keys(names: List<&2, String>, +info: ArgInfo) ->
2724    List<&2, OptKey>:
2725  match names:
2726    case Nil{}: Nil{}
2727    case name <> tail: opt_key(name, info) <> name_keys(tail, info)
2728
2729def nonempty_name(+name: String) -> List<&2, String>:
2730  Bool.pick(List<&2, String>, String.is_empty(name), [], [name])
2731
2732# The names option_matches accepts for info, in its order.
2733def info_names(info: ArgInfo) -> List<&2, String>:
2734  match info:
2735    case ArgInfo{id, usage, short, +long, kind, attrs}:
2736      List.append(&2, String, nonempty_name(short),
2737        List.append(&2, String, nonempty_name(long),
2738          List.append(&2, String, find_aliases(attrs),
2739            Bool.pick(List<&2, String>, Bool.and(kind_negatable(kind),
2740              Bool.not(String.is_empty(long))),
2741              [negated_long(long)], []))))
2742
2743def spec_keys(items: List<&2, SpecItem>) -> List<&2, OptKey>:
2744  match items:
2745    case Nil{}: Nil{}
2746    case SpecArg{+info} <> tail:
2747      List.append(&2, OptKey, name_keys(info_names(info), info),
2748        spec_keys(tail))
2749    case head <> tail: spec_keys(tail)
2750
2751# items less their Subs, each Arg's usage parsed once.
2752def spec_items(items: List<&2, Item>) -> List<&2, SpecItem>:
2753  match items:
2754    case Nil{}: Nil{}
2755    case Arg{usage, help, attrs} <> tail:
2756      SpecArg{arg_info(usage, attrs)} <> spec_items(tail)
2757    case Sub{name, about, children} <> tail: spec_items(tail)
2758    case head <> tail: SpecOther{head} <> spec_items(tail)
2759
2760# global_go over items parsed, with all as written.
2761def global_spec(items: List<&2, SpecItem>, +all: List<&2, Item>) ->
2762    List<&2, SpecItem>:
2763  match items:
2764    case Nil{}: Nil{}
2765    case SpecArg{+info} <> tail:
2766      +rest = global_spec(tail, all)
2767      attrs = info_attrs(info)
2768      Bool.pick(List<&2, SpecItem>, has_global(attrs),
2769        SpecArg{info} <> rest, rest)
2770    case SpecOther{Exclusive{+name}} <> tail:
2771      +rest = global_spec(tail, all)
2772      Bool.pick(List<&2, SpecItem>, group_global(all, name),
2773        SpecOther{Exclusive{name}} <> rest, rest)
2774    case SpecOther{NeedOne{+name}} <> tail:
2775      +rest = global_spec(tail, all)
2776      Bool.pick(List<&2, SpecItem>, group_global(all, name),
2777        SpecOther{NeedOne{name}} <> rest, rest)
2778    case head <> tail: global_spec(tail, all)
2779
2780def spec_slots(items: List<&2, SpecItem>) -> List<&2, ArgInfo>:
2781  match items:
2782    case Nil{}: Nil{}
2783    case SpecArg{+info} <> tail:
2784      +rest = spec_slots(tail)
2785      Bool.pick(List<&2, ArgInfo>, kind_pos(info_kind(info)),
2786        info <> rest, rest)
2787    case head <> tail: spec_slots(tail)
2788
2789def slots_variadic(slots: List<&2, ArgInfo>) -> Bool:
2790  match slots:
2791    case Nil{}: False{}
2792    case info <> tail:
2793      Bool.or(kind_variadic_pos(info_kind(info)), slots_variadic(tail))
2794
2795def info_digit_short(info: ArgInfo) -> Bool:
2796  match info:
2797    case ArgInfo{id, usage, short, long, kind, attrs}: short_digit(short)
2798
2799def spec_digit_short(items: List<&2, SpecItem>) -> Bool:
2800  match items:
2801    case Nil{}: False{}
2802    case SpecArg{info} <> tail:
2803      Bool.or(info_digit_short(info), spec_digit_short(tail))
2804    case head <> tail: spec_digit_short(tail)
2805
2806def raw_subs(items: List<&2, Item>) -> List<&2, SubKey>:
2807  match items:
2808    case Nil{}: Nil{}
2809    case Sub{name, about, +children} <> tail:
2810      SubKey{name, find_aliases(children), children} <> raw_subs(tail)
2811    case head <> tail: raw_subs(tail)
2812
2813# items as a scope inheriting globals; its subcommands stay unparsed until
2814# the scan enters one.
2815def spec_node(+items: List<&2, Item>, +globals: List<&2, SpecItem>) ->
2816    Spec:
2817  +own = spec_items(items)
2818  +args = List.append(&2, SpecItem, globals, own)
2819  +slots = spec_slots(own)
2820  Spec{List.append(&2, OptKey, spec_keys(own), spec_keys(globals)),
2821    args, slots, slots_variadic(slots), raw_subs(items),
2822    has_sub_item(items), has_external(items), needs_sub(items),
2823    has_version_item(items), spec_digit_short(args),
2824    List.append(&2, SpecItem, global_spec(own, items), globals)}
2825
2826def scan_start(c: Cmd) -> ScanState:
2827  match c:
2828    case Cmd{name, about, items}:
2829      ScanState{Scope{[], spec_node(items, [])}, [], False{}, 0n, None{},
2830        None{}}
2831
2832def scan_event(state: ScanState, event: ScanEvent) -> ScanState:
2833  match state:
2834    case ScanState{scope, events, resting, positional_count,
2835        external, issue}:
2836      ScanState{scope, event <> events, resting,
2837        positional_count, external, issue}
2838
2839def first_issue(issue: Maybe<&2, ScanFault>, fault: ScanFault) ->
2840    Maybe<&2, ScanFault>:
2841  match issue:
2842    case None{}: Some{fault}
2843    case Some{old}: Some{old}
2844
2845def scan_error(state: ScanState, fault: ScanFault) -> ScanState:
2846  match state:
2847    case ScanState{scope, events, resting, positional_count,
2848        external, issue}:
2849      ScanState{scope, events, resting, positional_count,
2850        external, first_issue(issue, fault)}
2851
2852def has_hyphen(items: List<&2, Item>) -> Bool:
2853  match items:
2854    case Nil{}: False{}
2855    case Hyphen{} <> tail: True{}
2856    case head <> tail: has_hyphen(tail)
2857
2858def slot_rest(found: Maybe<&2, ArgInfo>) -> Bool:
2859  match found:
2860    case None{}: False{}
2861    case Some{ArgInfo{id, usage, short, long, kind, attrs}}:
2862      has_rest(attrs)
2863
2864def slot_hyphen(found: Maybe<&2, ArgInfo>) -> Bool:
2865  match found:
2866    case None{}: False{}
2867    case Some{ArgInfo{id, usage, short, long, kind, attrs}}:
2868      has_hyphen(attrs)
2869
2870def has_as_f32(items: List<&2, Item>) -> Bool:
2871  match items:
2872    case Nil{}: False{}
2873    case As{TF32{}} <> tail: True{}
2874    case head <> tail: has_as_f32(tail)
2875
2876def slot_f32(found: Maybe<&2, ArgInfo>) -> Bool:
2877  match found:
2878    case None{}: False{}
2879    case Some{ArgInfo{id, usage, short, long, kind, attrs}}:
2880      has_as_f32(attrs)
2881
2882def is_negative_number(+s: String) -> Bool:
2883  Bool.and(String.starts_with(s, "-"), valid_f32(s))
2884
2885def before_equal(s: String) -> String:
2886  match s:
2887    case SNil{}: SNil{}
2888    case SCon{Chr{61}, tail}: SNil{}
2889    case SCon{head, tail}: SCon{head, before_equal(tail)}
2890
2891def after_equal(s: String) -> Maybe<&2, String>:
2892  match s:
2893    case SNil{}: None{}
2894    case SCon{Chr{61}, tail}: Some{tail}
2895    case SCon{head, tail}: after_equal(tail)
2896
2897def first_option_name(+token: String) -> String:
2898  Bool.pick(String, String.starts_with(token, "--"),
2899    before_equal(token), String.take(token, 2n))
2900
2901def scope_option_known(scope: Scope, token: String) -> Bool:
2902  Maybe.is_some(&2, OptFound,
2903    find_option_scope(scope, first_option_name(token)))
2904
2905def scope_builtin_known(scope: Scope, +token: String) -> Bool:
2906  match scope:
2907    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2908        external, needs_sub, version, digit_short, inherit}}:
2909      +name = first_option_name(token)
2910      Bool.or(String.eq(name, "-h"),
2911        Bool.or(String.eq(name, "--help"),
2912          Bool.and(List.is_empty(&2, String, path),
2913            Bool.and(version,
2914              Bool.or(String.eq(name, "-V"),
2915                String.eq(name, "--version"))))))
2916
2917def scope_any_option_known(+scope: Scope, +token: String) -> Bool:
2918  Bool.or(scope_option_known(scope, token),
2919    scope_builtin_known(scope, token))
2920
2921def scan_operand_ok(state: ScanState, token: String) -> ScanState:
2922  match state:
2923    case ScanState{+scope, events, resting, +positional_count,
2924        external, issue}:
2925      ScanState{scope, Operand{token} <> events,
2926        Bool.or(resting,
2927          slot_rest(scope_next_slot(scope, positional_count))),
2928        Nat.add(positional_count, 1n), external, issue}
2929
2930def scan_operand_case(allowed: Bool, state: ScanState,
2931    token: String) -> ScanState:
2932  match allowed:
2933    case True{}: scan_operand_ok(state, token)
2934    case False{}: scan_error(state, E09{token})
2935
2936def scope_can_take(scope: Scope, count: Nat) -> Bool:
2937  match scope:
2938    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2939        external, needs_sub, version, digit_short, inherit}}:
2940      Bool.or(Nat.is_lt(count, List.length(&2, ArgInfo, slots)), variadic)
2941
2942def scan_operand(state: ScanState, token: String) -> ScanState:
2943  match state:
2944    case ScanState{+scope, events, resting, +positional_count,
2945        external, issue}:
2946      scan_operand_case(scope_can_take(scope, positional_count),
2947        ScanState{scope, events, resting, positional_count,
2948          external, issue}, token)
2949
2950def spec_inherit(spec: Spec) -> List<&2, SpecItem>:
2951  match spec:
2952    case Spec{keys, args, slots, variadic, subs, has_sub, external,
2953        needs_sub, version, digit_short, inherit}: inherit
2954
2955def event_id(event: ScanEvent) -> Maybe<&2, String>:
2956  match event:
2957    case FlagHit{id, value}: Some{id}
2958    case CountHit{id}: Some{id}
2959    case ValueHit{id, value}: Some{id}
2960    case _: None{}
2961
2962def spec_info_found(item: SpecItem, id: String) -> Maybe<&2, ArgInfo>:
2963  match item:
2964    case SpecArg{+info}:
2965      own = info_id(info)
2966      Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
2967    case SpecOther{other}: None{}
2968
2969# find_arg_info over parsed items; after a hit it only walks the rest.
2970def find_spec_info(items: List<&2, SpecItem>, +id: String,
2971    found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
2972  match items found:
2973    case Nil{} hit: hit
2974    case item <> tail Some{hit}: Some{hit}
2975    case item <> tail None{}:
2976      find_spec_info(tail, id, spec_info_found(item, id))
2977
2978# An arg's hits from before the scan enters a subcommand stay only when the
2979# scope being left passes the arg down as a global (SPEC 8.4); operands,
2980# entries, help and version stay.
2981def event_inherited(found: Maybe<&2, String>,
2982    +globals: List<&2, SpecItem>) -> Bool:
2983  match found:
2984    case None{}: True{}
2985    case Some{id}:
2986      Maybe.is_some(&2, ArgInfo, find_spec_info(globals, id, None{}))
2987
2988def events_kept(keep: Bool, event: ScanEvent,
2989    rest: List<&2, ScanEvent>) -> List<&2, ScanEvent>:
2990  match keep:
2991    case True{}: event <> rest
2992    case False{}: rest
2993
2994def events_inherited(events: List<&2, ScanEvent>,
2995    +globals: List<&2, SpecItem>) -> List<&2, ScanEvent>:
2996  match events:
2997    case Nil{}: Nil{}
2998    case +event <> tail:
2999      events_kept(event_inherited(event_id(event), globals), event,
3000        events_inherited(tail, globals))
3001
3002def scan_descend(state: ScanState, sub: SubKey) -> ScanState:
3003  match state:
3004    case ScanState{scope, events, resting, positional_count,
3005        external, issue}:
3006      match scope:
3007        case Scope{path, spec}:
3008          match sub:
3009            case SubKey{+name, aliases, children}:
3010              +globals = spec_inherit(spec)
3011              ScanState{Scope{
3012                List.append(&2, String, path, [name]),
3013                spec_node(children, globals)},
3014                Enter{name} <> events_inherited(events, globals), resting, 0n,
3015                external, issue}
3016
3017def scan_external(state: ScanState, name: String,
3018    args: List<&2, String>) -> ScanState:
3019  match state:
3020    case ScanState{scope, events, resting, positional_count,
3021        external, issue}:
3022      ScanState{scope, events, resting, positional_count,
3023        Some{ExternalArgs{name, args}}, issue}
3024
3025# E04 quotes the first element that fails.
3026def scan_value_result(found: Maybe<&2, Bad>, state: ScanState,
3027    info: ArgInfo, value: String) -> ScanState:
3028  match found:
3029    case None{}:
3030      match info:
3031        case ArgInfo{id, usage, short, long, kind, attrs}:
3032          scan_event(state, ValueHit{id, value})
3033    case Some{Bad{bad, why}}:
3034      scan_error(state, E04{arg_display(info), bad, why})
3035
3036def scan_value(state: ScanState, +info: ArgInfo,
3037    +value: String) -> ScanState:
3038  scan_value_result(value_bad(info_attrs(info), value), state, info, value)
3039
3040def scan_simple_hit(state: ScanState, info: ArgInfo,
3041    polarity: Bool) -> ScanState:
3042  match info:
3043    case ArgInfo{id, usage, short, long, kind, attrs}:
3044      match kind:
3045        case CountKind{}: scan_event(state, CountHit{id})
3046        case _: scan_event(state, FlagHit{id, polarity})
3047
3048def scan_settle_case(resting: Bool, issue: Maybe<&2, ScanFault>,
3049    scope: Scope, events: List<&2, ScanEvent>, positional_count: Nat,
3050    external: Maybe<&2, ExternalArgs>) -> Mode:
3051  match resting issue:
3052    case True{} None{}:
3053      Operands{ScanState{scope, events, True{}, positional_count, external,
3054        None{}}, False{}}
3055    case True{} Some{fault}:
3056      Operands{ScanState{scope, events, True{}, positional_count, external,
3057        Some{fault}}, False{}}
3058    case False{} None{}:
3059      Open{ScanState{scope, events, False{}, positional_count, external,
3060        None{}}}
3061    case False{} Some{fault}:
3062      Closed{Faulted{ScanState{scope, events, False{}, positional_count,
3063        external, Some{fault}}}}
3064
3065def scan_settle(state: ScanState) -> Mode:
3066  match state:
3067    case ScanState{scope, events, resting, positional_count, external,
3068        issue}:
3069      scan_settle_case(resting, issue, scope, events, positional_count,
3070        external)
3071
3072def help_flag_case(long: Bool, short: Bool) -> Maybe<&2, Bool>:
3073  match long short:
3074    case True{} _: Some{True{}}
3075    case False{} True{}: Some{False{}}
3076    case False{} False{}: None{}
3077
3078# Which help flag a token is: Some{True{}} for --help, Some{False{}} for -h.
3079def help_flag(+token: String) -> Maybe<&2, Bool>:
3080  help_flag_case(String.eq(token, "--help"), String.eq(token, "-h"))
3081
3082def scan_faulted_case(help: Maybe<&2, Bool>, state: ScanState) -> Tail:
3083  match help:
3084    case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3085    case None{}: Faulted{state}
3086
3087def scan_faulted(state: ScanState, token: String) -> Tail:
3088  scan_faulted_case(help_flag(token), state)
3089
3090def scope_digit_short(scope: Scope) -> Bool:
3091  match scope:
3092    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3093        external, needs_sub, version, digit_short, inherit}}: digit_short
3094
3095def arg_has_hyphen(info: ArgInfo) -> Bool:
3096  match info:
3097    case ArgInfo{id, usage, short, long, kind, attrs}:
3098      has_hyphen(attrs)
3099
3100def arg_is_f32(info: ArgInfo) -> Bool:
3101  match info:
3102    case ArgInfo{id, usage, short, long, kind, attrs}:
3103      has_as_f32(attrs)
3104
3105def fetch_allowed(scope: Scope, +info: ArgInfo,
3106    +token: String) -> Bool:
3107  Bool.and(Bool.not(String.eq(token, "--")),
3108    Bool.or(Bool.not(String.starts_with(token, "-")),
3109      Bool.or(String.eq(token, "-"),
3110        Bool.or(arg_has_hyphen(info),
3111          Bool.and(arg_is_f32(info),
3112            Bool.and(is_negative_number(token),
3113              Bool.not(scope_digit_short(scope))))))))
3114
3115def scan_fetch_result(allowed: Bool, state: ScanState,
3116    info: ArgInfo, token: String) -> Mode:
3117  match allowed:
3118    case True{}: scan_settle(scan_value(state, info, token))
3119    case False{}:
3120      Closed{scan_faulted(scan_error(state, E03{arg_display(info)}), token)}
3121
3122def scan_state_scope(state: ScanState) -> Scope:
3123  match state:
3124    case ScanState{scope, events, resting, positional_count,
3125        external, issue}: scope
3126
3127def scan_fetch(+state: ScanState, +info: ArgInfo,
3128    +token: String) -> Mode:
3129  scan_fetch_result(fetch_allowed(scan_state_scope(state), info, token),
3130    state, info, token)
3131
3132def bare_value(attrs: List<&2, Item>) -> String:
3133  match attrs:
3134    case Nil{}: ""
3135    case Bare{value} <> tail: value
3136    case head <> tail: bare_value(tail)
3137
3138def scan_option(attached: Maybe<&2, String>, state: ScanState,
3139    info: ArgInfo) -> Mode:
3140  match attached:
3141    case Some{value}: scan_settle(scan_value(state, info, value))
3142    case None{}: Fetch{state, info}
3143
3144def info_bare_value(info: ArgInfo) -> String:
3145  match info:
3146    case ArgInfo{id, usage, short, long, kind, attrs}:
3147      bare_value(attrs)
3148
3149def scan_bare_option(attached: Maybe<&2, String>, state: ScanState,
3150    +info: ArgInfo) -> Mode:
3151  match attached:
3152    case Some{value}: scan_settle(scan_value(state, info, value))
3153    case None{}:
3154      scan_settle(scan_value(state, info, info_bare_value(info)))
3155
3156def scan_simple_option(attached: Maybe<&2, String>, state: ScanState,
3157    info: ArgInfo, polarity: Bool) -> Mode:
3158  match attached:
3159    case Some{value}:
3160      scan_settle(scan_error(state, E05{arg_display(info), value}))
3161    case None{}: scan_settle(scan_simple_hit(state, info, polarity))
3162
3163def scan_found_kind(kind: UsageKind, attached: Maybe<&2, String>,
3164    state: ScanState, info: ArgInfo, polarity: Bool) -> Mode:
3165  match kind:
3166    case FlagKind{}:
3167      scan_simple_option(attached, state, info, polarity)
3168    case CountKind{}:
3169      scan_simple_option(attached, state, info, polarity)
3170    case NegatableKind{}:
3171      scan_simple_option(attached, state, info, polarity)
3172    case OptionKind{many}:
3173      scan_option(attached, state, info)
3174    case BareOptionKind{}:
3175      scan_bare_option(attached, state, info)
3176    case PosKind{required, many}:
3177      scan_settle(state)
3178
3179def scan_found(found: OptFound, attached: Maybe<&2, String>,
3180    state: ScanState) -> Mode:
3181  match found:
3182    case OptFound{+arg, polarity}:
3183      scan_found_kind(info_kind(arg), attached, state, arg, polarity)
3184
3185def scope_version(scope: Scope) -> Bool:
3186  match scope:
3187    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3188        external, needs_sub, version, digit_short, inherit}}:
3189      Bool.and(List.is_empty(&2, String, path), version)
3190
3191def scan_builtin_hit(attached: Maybe<&2, String>, state: ScanState,
3192    event: ScanEvent, name: String) -> Mode:
3193  match attached:
3194    case Some{value}: scan_settle(scan_error(state, E05{name, value}))
3195    case None{}: Closed{Stopped{scan_event(state, event)}}
3196
3197def scan_builtin_version(enabled: Bool, attached: Maybe<&2, String>,
3198    state: ScanState) -> Mode:
3199  match enabled:
3200    case True{}:
3201      scan_builtin_hit(attached, state, VersionHit{}, "--version")
3202    case False{}:
3203      scan_settle(scan_error(state, E01{"--version"}))
3204
3205def scan_builtin_other(version: Bool, name: String,
3206    attached: Maybe<&2, String>, +state: ScanState) -> Mode:
3207  match version:
3208    case True{}:
3209      scan_builtin_version(scope_version(scan_state_scope(state)),
3210        attached, state)
3211    case False{}: scan_settle(scan_error(state, E01{name}))
3212
3213def scan_builtin_long_case(help: Bool, +name: String,
3214    attached: Maybe<&2, String>, state: ScanState) -> Mode:
3215  match help:
3216    case True{}:
3217      scan_builtin_hit(attached, state, HelpHit{True{}}, "--help")
3218    case False{}:
3219      scan_builtin_other(String.eq(name, "--version"), name, attached, state)
3220
3221def scan_builtin_long(+name: String, attached: Maybe<&2, String>,
3222    state: ScanState) -> Mode:
3223  scan_builtin_long_case(String.eq(name, "--help"), name, attached, state)
3224
3225def scan_long_found(found: Maybe<&2, OptFound>, name: String,
3226    attached: Maybe<&2, String>, state: ScanState) -> Mode:
3227  match found:
3228    case Some{option}: scan_found(option, attached, state)
3229    case None{}: scan_builtin_long(name, attached, state)
3230
3231def scan_long(+token: String, +state: ScanState) -> Mode:
3232  +name = before_equal(token)
3233  scan_long_found(find_option_scope(scan_state_scope(state), name),
3234    name, after_equal(token), state)
3235
3236def find_short_for_head(chars: String, state: ScanState) ->
3237    Maybe<&2, OptFound>:
3238  match chars:
3239    case SNil{}: None{}
3240    case SCon{head, tail}:
3241      find_option_scope(scan_state_scope(state),
3242        String.append("-", Char.show(head)))
3243
3244def short_attached(chars: String) -> String:
3245  match chars:
3246    case SCon{Chr{61}, tail}: tail
3247    case _: chars
3248
3249def scan_short_value_attached(value: String, state: ScanState,
3250    info: ArgInfo) -> Mode:
3251  match value:
3252    case SNil{}: Fetch{state, info}
3253    case _: scan_settle(scan_value(state, info, value))
3254
3255def scan_short_value(chars: String, state: ScanState,
3256    info: ArgInfo) -> Mode:
3257  scan_short_value_attached(short_attached(chars), state, info)
3258
3259def scan_short_bare_value(chars: String, state: ScanState,
3260    +info: ArgInfo) -> Mode:
3261  match chars:
3262    case SNil{}:
3263      scan_settle(scan_value(state, info, info_bare_value(info)))
3264    case _:
3265      scan_settle(scan_value(state, info, short_attached(chars)))
3266
3267def scan_short_version(enabled: Bool, state: ScanState) -> Mode:
3268  match enabled:
3269    case True{}: Closed{Stopped{scan_event(state, VersionHit{})}}
3270    case False{}: scan_settle(scan_error(state, E01{"-V"}))
3271
3272def scan_short_go(chars: String, +state: ScanState,
3273    found: Maybe<&2, OptFound>) -> Mode:
3274  match chars found:
3275    case SNil{} _:
3276      scan_settle(state)
3277    case SCon{Chr{104}, +tail} None{}:
3278      Closed{Stopped{scan_event(state, HelpHit{False{}})}}
3279    case SCon{Chr{86}, tail} None{}:
3280      scan_short_version(scope_version(scan_state_scope(state)), state)
3281    case SCon{head, tail} None{}:
3282      scan_settle(scan_error(state,
3283        E01{String.append("-", Char.show(head))}))
3284    case SCon{head, tail} Some{OptFound{arg, polarity}}:
3285      match arg:
3286        case ArgInfo{id, usage, short, long, kind, attrs}:
3287          match kind:
3288            case FlagKind{}:
3289              +next = scan_event(state, FlagHit{id, polarity})
3290              scan_short_go(tail, next, find_short_for_head(tail, next))
3291            case CountKind{}:
3292              +next = scan_event(state, CountHit{id})
3293              scan_short_go(tail, next, find_short_for_head(tail, next))
3294            case NegatableKind{}:
3295              +next = scan_event(state, FlagHit{id, polarity})
3296              scan_short_go(tail, next, find_short_for_head(tail, next))
3297            case OptionKind{many}:
3298              scan_short_value(tail, state,
3299                ArgInfo{id, usage, short, long, OptionKind{many}, attrs})
3300            case BareOptionKind{}:
3301              scan_short_bare_value(tail, state,
3302                ArgInfo{id, usage, short, long, BareOptionKind{}, attrs})
3303            case PosKind{required, many}:
3304              scan_settle(state)
3305
3306def scan_short(token: String, +state: ScanState) -> Mode:
3307  +chars = String.drop(token, 1n)
3308  scan_short_go(chars, state, find_short_for_head(chars, state))
3309
3310def scope_external(scope: Scope) -> Bool:
3311  match scope:
3312    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3313        external, needs_sub, version, digit_short, inherit}}: external
3314
3315def scope_has_sub(scope: Scope) -> Bool:
3316  match scope:
3317    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3318        external, needs_sub, version, digit_short, inherit}}: has_sub
3319
3320def sub_key_aliased(aliased: Bool, name: String, aliases: List<&2, String>,
3321    children: List<&2, Item>) -> Maybe<&2, SubKey>:
3322  match aliased:
3323    case True{}: Some{SubKey{name, aliases, children}}
3324    case False{}: None{}
3325
3326def sub_key_hit(same: Bool, name: String, +aliases: List<&2, String>,
3327    children: List<&2, Item>, query: String) -> Maybe<&2, SubKey>:
3328  match same:
3329    case True{}: Some{SubKey{name, aliases, children}}
3330    case False{}:
3331      sub_key_aliased(List.contains(~String, ~String.eq, aliases, query),
3332        name, aliases, children)
3333
3334def sub_key_found(key: SubKey, +query: String) -> Maybe<&2, SubKey>:
3335  match key:
3336    case SubKey{+name, aliases, children}:
3337      sub_key_hit(same_text(query, name), name, aliases, children, query)
3338
3339# find_sub over a scope's subcommands; after a hit it only walks the rest.
3340def find_sub_key(subs: List<&2, SubKey>, +query: String,
3341    found: Maybe<&2, SubKey>) -> Maybe<&2, SubKey>:
3342  match subs found:
3343    case Nil{} hit: hit
3344    case key <> tail Some{hit}: Some{hit}
3345    case key <> tail None{}:
3346      find_sub_key(tail, query, sub_key_found(key, query))
3347
3348def scope_find_sub(scope: Scope, query: String) -> Maybe<&2, SubKey>:
3349  match scope:
3350    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3351        external, needs_sub, version, digit_short, inherit}}:
3352      find_sub_key(subs, query, None{})
3353
3354def scan_sub_unknown(allow_external: Bool, state: ScanState,
3355    token: String) -> Mode:
3356  match allow_external:
3357    case True{}: Capture{state, token}
3358    case False{}: Closed{Stopped{scan_error(state, E02{token})}}
3359
3360def scan_sub_found(found: Maybe<&2, SubKey>, +state: ScanState,
3361    token: String) -> Mode:
3362  match found:
3363    case Some{sub}: scan_settle(scan_descend(state, sub))
3364    case None{}:
3365      scan_sub_unknown(scope_external(scan_state_scope(state)),
3366        state, token)
3367
3368def unknown_fault(dash: Bool, token: String) -> ScanFault:
3369  match dash:
3370    case True{}: E01{token}
3371    case False{}: E02{token}
3372
3373def help_missing_case(help: Maybe<&2, Bool>, +token: String,
3374    state: ScanState) -> Tail:
3375  match help:
3376    case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3377    case None{}:
3378      Stopped{scan_error(state,
3379        unknown_fault(String.starts_with(token, "-"), token))}
3380
3381def help_missing(+token: String, state: ScanState) -> Tail:
3382  help_missing_case(help_flag(token), token, state)
3383
3384def help_walk(found: Maybe<&2, SubKey>, state: ScanState,
3385    token: String) -> Tail:
3386  match found:
3387    case Some{sub}: Walk{scan_descend(state, sub)}
3388    case None{}: help_missing(token, state)
3389
3390def scan_non_option_sub_case(help: Bool, +token: String,
3391    +state: ScanState) -> Mode:
3392  match help:
3393    case True{}: Closed{Walk{state}}
3394    case False{}:
3395      scan_sub_found(scope_find_sub(scan_state_scope(state), token),
3396        state, token)
3397
3398def scan_non_option_sub(+token: String, state: ScanState) -> Mode:
3399  scan_non_option_sub_case(String.eq(token, "help"), token, state)
3400
3401def scan_non_option_case(has_sub: Bool, token: String,
3402    state: ScanState) -> Mode:
3403  match has_sub:
3404    case True{}: scan_non_option_sub(token, state)
3405    case False{}: scan_settle(scan_operand(state, token))
3406
3407def scan_non_option(token: String, +state: ScanState) -> Mode:
3408  scan_non_option_case(scope_has_sub(scan_state_scope(state)), token, state)
3409
3410def scan_hyphen_candidate(state: ScanState, +token: String) -> Bool:
3411  match state:
3412    case ScanState{+scope, events, resting, positional_count,
3413        external, issue}:
3414      +slot = scope_next_slot(scope, positional_count)
3415      Bool.or(Bool.and(slot_hyphen(slot),
3416        Bool.not(scope_any_option_known(scope, token))),
3417        Bool.and(slot_f32(slot),
3418          Bool.and(is_negative_number(token),
3419            Bool.not(scope_digit_short(scope)))))
3420
3421def scan_dash_case(operand: Bool, token: String, state: ScanState) -> Mode:
3422  match operand:
3423    case True{}: scan_settle(scan_operand(state, token))
3424    case False{}: scan_short(token, state)
3425
3426def scan_dash_token(+token: String, +state: ScanState) -> Mode:
3427  scan_dash_case(scan_hyphen_candidate(state, token), token, state)
3428
3429# How an open scan reads a token: a long option, a dash token (a cluster of
3430# short options, or an operand a slot takes), or a word; "-" is a word.
3431type TokenClass is Data:
3432  LongToken{}
3433  DashToken{}
3434  WordToken{}
3435
3436def token_class(token: String) -> TokenClass:
3437  match token:
3438    case SCon{Chr{45}, SCon{Chr{45}, rest}}: LongToken{}
3439    case SCon{Chr{45}, SNil{}}: WordToken{}
3440    case SCon{Chr{45}, rest}: DashToken{}
3441    case _: WordToken{}
3442
3443def scan_token_case(kind: TokenClass, token: String,
3444    state: ScanState) -> Mode:
3445  match kind:
3446    case LongToken{}: scan_long(token, state)
3447    case DashToken{}: scan_dash_token(token, state)
3448    case WordToken{}: scan_non_option(token, state)
3449
3450def scan_token(+token: String, state: ScanState) -> Mode:
3451  scan_token_case(token_class(token), token, state)
3452
3453def scan_tail(tail: Tail, +token: String) -> Tail:
3454  match tail:
3455    case Faulted{state}: scan_faulted(state, token)
3456    case Walk{+state}:
3457      help_walk(scope_find_sub(scan_state_scope(state), token),
3458        state, token)
3459    case Stopped{state}: Stopped{state}
3460
3461# `--` ends options only here, while the scan is open (R1).
3462def scan_open(dash: Bool, state: ScanState, token: String) -> Mode:
3463  match dash:
3464    case True{}: Operands{state, True{}}
3465    case False{}: scan_token(token, state)
3466
3467# An external subcommand takes the rest of argv verbatim.
3468def scan_go(tokens: List<&2, String>, mode: Mode) -> Mode:
3469  match tokens mode:
3470    case argv Capture{state, name}:
3471      Closed{Stopped{scan_external(state, name, argv)}}
3472    case Nil{} _: mode
3473    case +token <> rest Open{state}:
3474      scan_go(rest, scan_open(String.eq(token, "--"), state, token))
3475    case token <> rest Fetch{state, info}:
3476      scan_go(rest, scan_fetch(state, info, token))
3477    case token <> rest Operands{state, ended}:
3478      scan_go(rest, Operands{scan_operand(state, token), ended})
3479    case token <> rest Closed{tail}:
3480      scan_go(rest, Closed{scan_tail(tail, token)})
3481
3482def scan_tail_close(tail: Tail) -> ScanState:
3483  match tail:
3484    case Faulted{state}: state
3485    case Walk{state}: scan_event(state, HelpHit{True{}})
3486    case Stopped{state}: state
3487
3488def scan_close(mode: Mode) -> ScanState:
3489  match mode:
3490    case Open{state}: state
3491    case Fetch{state, info}: scan_error(state, E03{arg_display(info)})
3492    case Operands{state, ended}: state
3493    case Capture{state, name}: scan_external(state, name, [])
3494    case Closed{tail}: scan_tail_close(tail)
3495
3496def scan_trace_of(state: ScanState, ended: Bool) -> ScanTrace:
3497  match state:
3498    case ScanState{scope, events, resting, positional_count,
3499        external, issue}:
3500      match scope:
3501        case Scope{path, spec}:
3502          ScanTrace{path, List.reverse(&2, ScanEvent, events),
3503            ended, external, issue}
3504
3505def scan_trace(mode: Mode) -> ScanTrace:
3506  match mode:
3507    case Operands{state, ended}: scan_trace_of(state, ended)
3508    case other: scan_trace_of(scan_close(other), False{})
3509
3510def scan(c: Cmd, argv: List<&2, String>) -> ScanTrace:
3511  scan_trace(scan_go(argv, Open{scan_start(c)}))
3512
3513def find_early(events: List<&2, ScanEvent>) -> Maybe<&2, Early>:
3514  match events:
3515    case Nil{}: None{}
3516    case HelpHit{long} <> tail: Some{EarlyHelp{long}}
3517    case VersionHit{} <> tail: Some{EarlyVersion{}}
3518    case event <> tail: find_early(tail)
3519
3520def scope_needs_sub(scope: Scope) -> Bool:
3521  match scope:
3522    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3523        external, needs_sub, version, digit_short, inherit}}: needs_sub
3524
3525def bool_text(value: Bool) -> String:
3526  Bool.pick(String, value, "true", "false")
3527
3528def gather_value(gather: Gather, +value: String,
3529    many: Bool) -> Gather:
3530  match gather:
3531    case Gather{seen, values, +count}:
3532      Bool.pick(Gather, many,
3533        Gather{True{}, value <> values, count},
3534        Gather{True{}, [value], count})
3535
3536def gather_flag(gather: Gather, value: Bool) -> Gather:
3537  match gather:
3538    case Gather{seen, values, count}:
3539      Gather{True{}, [bool_text(value)], count}
3540
3541def gather_count(gather: Gather) -> Gather:
3542  match gather:
3543    case Gather{seen, values, count}:
3544      +next = Nat.add(count, 1n)
3545      Gather{True{}, [Nat.show(next)], next}
3546
3547def kind_many(kind: UsageKind) -> Bool:
3548  match kind:
3549    case OptionKind{many}: many
3550    case PosKind{required, many}: many
3551    case _: False{}
3552
3553def gather_event(event: ScanEvent, id: String,
3554    kind: UsageKind, +gather: Gather) -> Gather:
3555  match event:
3556    case ValueHit{key, value}:
3557      Bool.pick(Gather, String.eq(key, id),
3558        gather_value(gather, value, kind_many(kind)), gather)
3559    case FlagHit{key, value}:
3560      Bool.pick(Gather, String.eq(key, id),
3561        gather_flag(gather, value), gather)
3562    case CountHit{key}:
3563      Bool.pick(Gather, String.eq(key, id),
3564        gather_count(gather), gather)
3565    case _: gather
3566
3567def gather_events(events: List<&2, ScanEvent>, +id: String,
3568    +kind: UsageKind, gather: Gather) -> Gather:
3569  match events:
3570    case Nil{}: gather
3571    case event <> tail:
3572      gather_events(tail, id, kind,
3573        gather_event(event, id, kind, gather))
3574
3575def gather_arg(events: List<&2, ScanEvent>, info: ArgInfo) -> Gather:
3576  match info:
3577    case ArgInfo{id, usage, short, long, kind, attrs}:
3578      gather_events(events, id, kind, Gather{False{}, [], 0n})
3579
3580# Each env() name, in order.
3581def find_envs(attrs: List<&2, Item>) -> List<&2, String>:
3582  match attrs:
3583    case Nil{}: Nil{}
3584    case AttrEnv{name} <> tail: name <> find_envs(tail)
3585    case head <> tail: find_envs(tail)
3586
3587def env_present(name: String, found: Maybe<&2, String>) ->
3588    Maybe<&2, EnvValue>:
3589  match found:
3590    case None{}: None{}
3591    case Some{+value}:
3592      Bool.pick(Maybe<&2, EnvValue>, String.is_empty(value),
3593        None{}, Some{EnvValue{name, value}})
3594
3595# found, or else name's value in vars if it is set and not empty.
3596def env_next(found: Maybe<&2, EnvValue>, +name: String,
3597    vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3598  match found:
3599    case Some{env}: Some{env}
3600    case None{}: env_present(name, var_value(vars, name))
3601
3602# found, or else the first of names that is set and not empty in vars.
3603def env_first(names: List<&2, String>, found: Maybe<&2, EnvValue>,
3604    +vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3605  match names:
3606    case Nil{}: found
3607    case name <> tail: env_first(tail, env_next(found, name, vars), vars)
3608
3609def host_env(h: Host, names: List<&2, String>) -> Maybe<&2, EnvValue>:
3610  match h:
3611    case Host{width, vars, out_tty, err_tty}:
3612      env_first(names, None{}, vars)
3613
3614def env_bool_true(value: String) -> Bool:
3615  List.contains(~String, ~String.eq,
3616    ["true", "yes", "on", "1"], String.to_lower(value))
3617
3618def env_values(kind: UsageKind, attrs: List<&2, Item>,
3619    value: String) -> List<&2, String>:
3620  match kind:
3621    case FlagKind{}: [bool_text(env_bool_true(value))]
3622    case NegatableKind{}: [bool_text(env_bool_true(value))]
3623    case CountKind{}: [value]
3624    case _: apply_delim(find_delim(attrs), [value])
3625
3626def resolve_env_valid(found: Maybe<&2, Bad>, info: ArgInfo,
3627    name: String, value: String) -> FieldBuild:
3628  match found:
3629    case Some{Bad{bad, why}}:
3630      BuildFault{E11{arg_display(info), bad, name, why}}
3631    case None{}:
3632      match info:
3633        case ArgInfo{id, usage, short, long, kind, attrs}:
3634          Built{[Field{id, env_values(kind, attrs, value), Env{}}]}
3635
3636def resolve_env(+info: ArgInfo, env: EnvValue) -> FieldBuild:
3637  match env:
3638    case EnvValue{name, +value}:
3639      resolve_env_valid(value_issue(info_kind(info), info_attrs(info), value),
3640        info, name, value)
3641
3642def resolve_default(found: Maybe<&2, String>,
3643    info: ArgInfo) -> FieldBuild:
3644  match found:
3645    case None{}: Built{[]}
3646    case Some{value}:
3647      match info:
3648        case ArgInfo{id, usage, short, long, kind, attrs}:
3649          Built{[Field{id, env_values(kind, attrs, value), Default{}}]}
3650
3651def info_default(info: ArgInfo) -> Maybe<&2, String>:
3652  match info:
3653    case ArgInfo{id, usage, short, long, kind, attrs}:
3654      find_default(attrs)
3655
3656def resolve_env_default(found: Maybe<&2, EnvValue>,
3657    +info: ArgInfo) -> FieldBuild:
3658  match found:
3659    case Some{env}: resolve_env(info, env)
3660    case None{}: resolve_default(info_default(info), info)
3661
3662def info_envs(info: ArgInfo) -> List<&2, String>:
3663  match info:
3664    case ArgInfo{id, usage, short, long, kind, attrs}:
3665      find_envs(attrs)
3666
3667def resolve_argv_gather(gather: Gather, +info: ArgInfo,
3668    h: Host) -> FieldBuild:
3669  match gather:
3670    case Gather{True{}, values, count}:
3671      match info:
3672        case ArgInfo{id, usage, short, long, kind, attrs}:
3673          Built{[Field{id, apply_delim(find_delim(attrs),
3674            List.reverse(&2, String, values)), Argv{}}]}
3675    case Gather{False{}, values, count}:
3676      resolve_env_default(host_env(h, info_envs(info)), info)
3677
3678def resolve_option(+info: ArgInfo,
3679    events: List<&2, ScanEvent>, h: Host) -> FieldBuild:
3680  resolve_argv_gather(gather_arg(events, info), info, h)
3681
3682def scope_arg_items(scope: Scope) -> List<&2, SpecItem>:
3683  match scope:
3684    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3685        external, needs_sub, version, digit_short, inherit}}: args
3686
3687def combine_built(fields: List<&2, Field>,
3688    next: FieldBuild) -> FieldBuild:
3689  match next:
3690    case Built{more}:
3691      Built{List.append(&2, Field, fields, more)}
3692    case BuildFault{fault}: BuildFault{fault}
3693
3694def combine_build(first: FieldBuild,
3695    second: FieldBuild) -> FieldBuild:
3696  match first:
3697    case Built{fields}: combine_built(fields, second)
3698    case BuildFault{fault}: BuildFault{fault}
3699
3700def resolve_arg(pos: Bool, +info: ArgInfo, events: List<&2, ScanEvent>,
3701    h: Host, later: FieldBuild) -> FieldBuild:
3702  match pos:
3703    case True{}: later
3704    case False{}: combine_build(resolve_option(info, events, h), later)
3705
3706def resolve_options(items: List<&2, SpecItem>,
3707    +events: List<&2, ScanEvent>, +h: Host) -> FieldBuild:
3708  match items:
3709    case Nil{}: Built{[]}
3710    case SpecArg{+info} <> tail:
3711      resolve_arg(kind_pos(info_kind(info)), info, events, h,
3712        resolve_options(tail, events, h))
3713    case head <> tail: resolve_options(tail, events, h)
3714
3715def scan_operands(events: List<&2, ScanEvent>) ->
3716    List<&2, String>:
3717  match events:
3718    case Nil{}: Nil{}
3719    case Operand{token} <> tail:
3720      token <> scan_operands(tail)
3721    case event <> tail: scan_operands(tail)
3722
3723def position_take_count(kind: UsageKind,
3724    after: List<&2, ArgInfo>, +tokens: List<&2, String>) -> Nat:
3725  match kind:
3726    case PosKind{required, True{}}:
3727      Nat.sub(List.length(&2, String, tokens),
3728        List.length(&2, ArgInfo, after))
3729    case _:
3730      Bool.pick(Nat, List.is_empty(&2, String, tokens), 0n, 1n)
3731
3732def pos_valid_result(found: Maybe<&2, Bad>,
3733    info: ArgInfo, values: List<&2, String>) -> FieldBuild:
3734  match found:
3735    case Some{Bad{bad, why}}:
3736      BuildFault{E04{arg_display(info), bad, why}}
3737    case None{}:
3738      match info:
3739        case ArgInfo{id, usage, short, long, kind, attrs}:
3740          Built{[Field{id, values, Argv{}}]}
3741
3742def resolve_pos(+info: ArgInfo,
3743    +values: List<&2, String>, h: Host) -> FieldBuild:
3744  match values:
3745    case Nil{}:
3746      resolve_env_default(host_env(h, info_envs(info)), info)
3747    case head <> tail:
3748      +attrs = info_attrs(info)
3749      +split = apply_delim(find_delim(attrs), values)
3750      pos_valid_result(first_bad(split, attrs), info, split)
3751
3752def resolve_positions(slots: List<&2, ArgInfo>,
3753    +tokens: List<&2, String>, +h: Host) -> FieldBuild:
3754  match slots:
3755    case Nil{}: Built{[]}
3756    case +info <> +tail:
3757      +amount = position_take_count(info_kind(info), tail, tokens)
3758      combine_build(
3759        resolve_pos(info, List.take(&2, String, tokens, amount), h),
3760        resolve_positions(tail, List.drop(&2, String, tokens, amount), h))
3761
3762def scope_positions(scope: Scope) -> List<&2, ArgInfo>:
3763  match scope:
3764    case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3765        external, needs_sub, version, digit_short, inherit}}: slots
3766
3767def resolve_scope(+scope: Scope, +events: List<&2, ScanEvent>,
3768    +h: Host) -> FieldBuild:
3769  combine_build(
3770    resolve_options(scope_arg_items(scope), events, h),
3771    resolve_positions(scope_positions(scope),
3772      scan_operands(events), h))
3773
3774def source_given(source: Src) -> Bool:
3775  match source:
3776    case Argv{}: True{}
3777    case Env{}: True{}
3778    case Default{}: False{}
3779    case Absent{}: False{}
3780
3781def field_source(fields: List<&2, Field>, +id: String) -> Src:
3782  match fields:
3783    case Nil{}: Absent{}
3784    case Field{key, values, source} <> tail:
3785      Bool.pick(Src, String.eq(key, id), source,
3786        field_source(tail, id))
3787
3788def field_given(fields: List<&2, Field>, id: String) -> Bool:
3789  source_given(field_source(fields, id))
3790
3791def source_present(source: Src) -> Bool:
3792  match source:
3793    case Absent{}: False{}
3794    case _: True{}
3795
3796def field_present(fields: List<&2, Field>, id: String) -> Bool:
3797  source_present(field_source(fields, id))
3798
3799def group_usages(items: List<&2, Item>, +name: String) ->
3800    List<&2, String>:
3801  match items:
3802    case Nil{}: Nil{}
3803    case Arg{+usage, help, +attrs} <> tail:
3804      +later = group_usages(tail, name)
3805      Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3806        arg_display(arg_info(usage, attrs)) <> later, later)
3807    case head <> tail: group_usages(tail, name)
3808
3809def spec_group_usages(items: List<&2, SpecItem>, +name: String) ->
3810    List<&2, String>:
3811  match items:
3812    case Nil{}: Nil{}
3813    case SpecArg{+info} <> tail:
3814      +later = spec_group_usages(tail, name)
3815      attrs = info_attrs(info)
3816      Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3817        arg_display(info) <> later, later)
3818    case head <> tail: spec_group_usages(tail, name)
3819
3820def spec_group_display(items: List<&2, SpecItem>, name: String) -> String:
3821  String.concat(["<", String.join(spec_group_usages(items, name), "|"), ">"])
3822
3823def group_given(items: List<&2, SpecItem>,
3824    +fields: List<&2, Field>, +name: String) -> Bool:
3825  match items:
3826    case Nil{}: False{}
3827    case SpecArg{ArgInfo{id, usage, short, long, kind, attrs}} <> tail:
3828      Bool.or(Bool.and(arg_in_group(attrs, name), field_given(fields, id)),
3829        group_given(tail, fields, name))
3830    case head <> tail: group_given(tail, fields, name)
3831
3832def arg_required_missing(info: ArgInfo,
3833    +fields: List<&2, Field>) -> Bool:
3834  match info:
3835    case ArgInfo{+id, usage, short, long, kind, attrs}:
3836      Bool.or(Bool.and(has_required(attrs),
3837        Bool.not(field_given(fields, id))),
3838        Bool.and(kind_required_pos(kind),
3839          Bool.not(field_present(fields, id))))
3840
3841def group_display(items: List<&2, Item>, name: String) -> String:
3842  String.concat(["<", String.join(group_usages(items, name), "|"), ">"])
3843
3844def missing_arg(+info: ArgInfo,
3845    fields: List<&2, Field>) -> List<&2, String>:
3846  Bool.pick(List<&2, String>,
3847    arg_required_missing(info, fields),
3848    [arg_display(info)], [])
3849
3850def missing_group(+items: List<&2, SpecItem>,
3851    fields: List<&2, Field>, +name: String) -> List<&2, String>:
3852  Bool.pick(List<&2, String>,
3853    group_given(items, fields, name), [],
3854    [spec_group_display(items, name)])
3855
3856# A group's display the first time a need_one rule names it.
3857def missing_once(listed: Bool, items: List<&2, SpecItem>,
3858    fields: List<&2, Field>, name: String) -> List<&2, String>:
3859  match listed:
3860    case True{}: []
3861    case False{}: missing_group(items, fields, name)
3862
3863# What E06 lists, in items' order: each required arg not given, and each
3864# need_one group with no arg given that done does not name yet.
3865def missing_items(items: List<&2, SpecItem>,
3866    +all_items: List<&2, SpecItem>,
3867    +fields: List<&2, Field>, +done: List<&2, String>) ->
3868    List<&2, String>:
3869  match items:
3870    case Nil{}: Nil{}
3871    case SpecArg{info} <> tail:
3872      List.append(&2, String,
3873        missing_arg(info, fields),
3874        missing_items(tail, all_items, fields, done))
3875    case SpecOther{NeedOne{+name}} <> tail:
3876      List.append(&2, String,
3877        missing_once(List.contains(~String, ~String.eq, done, name),
3878          all_items, fields, name),
3879        missing_items(tail, all_items, fields, name <> done))
3880    case head <> tail:
3881      missing_items(tail, all_items, fields, done)
3882
3883def scope_missing(scope: Scope,
3884    fields: List<&2, Field>) -> List<&2, String>:
3885  +items = scope_arg_items(scope)
3886  missing_items(items, items, fields, [])
3887
3888def append_unique(+ids: List<&2, String>, +id: String) ->
3889    List<&2, String>:
3890  Bool.pick(List<&2, String>,
3891    List.contains(~String, ~String.eq, ids, id), ids,
3892    List.append(&2, String, ids, [id]))
3893
3894def append_event_id(found: Maybe<&2, String>,
3895    ids: List<&2, String>) -> List<&2, String>:
3896  match found:
3897    case None{}: ids
3898    case Some{id}: append_unique(ids, id)
3899
3900def argv_order(events: List<&2, ScanEvent>,
3901    ids: List<&2, String>) -> List<&2, String>:
3902  match events:
3903    case Nil{}: ids
3904    case event <> tail:
3905      argv_order(tail, append_event_id(event_id(event), ids))
3906
3907def source_env(source: Src) -> Bool:
3908  match source:
3909    case Env{}: True{}
3910    case _: False{}
3911
3912def env_order(fields: List<&2, Field>,
3913    +ids: List<&2, String>) -> List<&2, String>:
3914  match fields:
3915    case Nil{}: ids
3916    case Field{id, values, source} <> tail:
3917      env_order(tail, Bool.pick(List<&2, String>,
3918        source_env(source), append_unique(ids, id), ids))
3919
3920def present_order(events: List<&2, ScanEvent>,
3921    fields: List<&2, Field>) -> List<&2, String>:
3922  env_order(fields, argv_order(events, []))
3923
3924def arg_info_found(item: Item, id: String) -> Maybe<&2, ArgInfo>:
3925  match item:
3926    case Arg{usage, help, attrs}:
3927      +info = arg_info(usage, attrs)
3928      own = info_id(info)
3929      Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
3930    case _: None{}
3931
3932# The first Arg whose id is id; after a hit it walks no further.
3933def find_arg_go(items: List<&2, Item>, +id: String,
3934    found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
3935  match items found:
3936    case _ Some{hit}: Some{hit}
3937    case Nil{} None{}: None{}
3938    case item <> tail None{}: find_arg_go(tail, id, arg_info_found(item, id))
3939
3940def find_arg_info(items: List<&2, Item>, +id: String) ->
3941    Maybe<&2, ArgInfo>:
3942  find_arg_go(items, id, None{})
3943
3944def exclusive_groups(items: List<&2, SpecItem>) -> List<&2, String>:
3945  match items:
3946    case Nil{}: Nil{}
3947    case SpecOther{Exclusive{name}} <> tail:
3948      name <> exclusive_groups(tail)
3949    case head <> tail: exclusive_groups(tail)
3950
3951def common_exclusive(groups: List<&2, String>,
3952    +left: List<&2, String>, +right: List<&2, String>) -> Bool:
3953  match groups:
3954    case Nil{}: False{}
3955    case +name <> tail:
3956      Bool.or(Bool.and(
3957        List.contains(~String, ~String.eq, left, name),
3958        List.contains(~String, ~String.eq, right, name)),
3959        common_exclusive(tail, left, right))
3960
3961def pair_conflicts(+later: ArgInfo, +earlier: ArgInfo,
3962    groups: List<&2, String>) -> Bool:
3963  +later_attrs = info_attrs(later)
3964  +earlier_attrs = info_attrs(earlier)
3965  Bool.or(List.contains(~String, ~String.eq,
3966      find_conflicts(later_attrs), info_id(earlier)),
3967    Bool.or(List.contains(~String, ~String.eq,
3968        find_conflicts(earlier_attrs), info_id(later)),
3969      common_exclusive(groups,
3970        find_group_attrs(later_attrs),
3971        find_group_attrs(earlier_attrs))))
3972
3973def pair_fault(+later: ArgInfo, earlier: Maybe<&2, ArgInfo>,
3974    groups: List<&2, String>) -> Maybe<&2, ScanFault>:
3975  match earlier:
3976    case None{}: None{}
3977    case Some{+prior}:
3978      Bool.pick(Maybe<&2, ScanFault>,
3979        pair_conflicts(later, prior, groups),
3980        Some{E07{arg_display(later), arg_display(prior)}}, None{})
3981
3982def earlier_conflict(earlier_ids: List<&2, String>,
3983    +later: ArgInfo, +items: List<&2, SpecItem>,
3984    +groups: List<&2, String>,
3985    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
3986  match earlier_ids found:
3987    case Nil{} Some{fault}: Some{fault}
3988    case Nil{} None{}: None{}
3989    case id <> tail Some{fault}: Some{fault}
3990    case id <> tail None{}:
3991      earlier_conflict(tail, later, items, groups,
3992        pair_fault(later, find_spec_info(items, id, None{}), groups))
3993
3994def conflict_for_id(info: Maybe<&2, ArgInfo>,
3995    seen: List<&2, String>, items: List<&2, SpecItem>,
3996    groups: List<&2, String>) -> Maybe<&2, ScanFault>:
3997  match info:
3998    case None{}: None{}
3999    case Some{later}:
4000      earlier_conflict(seen, later, items, groups, None{})
4001
4002def conflict_go(ids: List<&2, String>,
4003    +seen: List<&2, String>, +items: List<&2, SpecItem>,
4004    +groups: List<&2, String>,
4005    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4006  match ids found:
4007    case Nil{} Some{fault}: Some{fault}
4008    case Nil{} None{}: None{}
4009    case id <> tail Some{fault}: Some{fault}
4010    case +id <> tail None{}:
4011      conflict_go(tail, List.append(&2, String, seen, [id]),
4012        items, groups,
4013        conflict_for_id(find_spec_info(items, id, None{}),
4014          seen, items, groups))
4015
4016def require_target_fault(target: Maybe<&2, ArgInfo>,
4017    +requester: ArgInfo, fields: List<&2, Field>) ->
4018    Maybe<&2, ScanFault>:
4019  match target:
4020    case None{}: None{}
4021    case Some{+needed}:
4022      Bool.pick(Maybe<&2, ScanFault>,
4023        Bool.not(field_given(fields, info_id(needed))),
4024        Some{E08{arg_display(requester), arg_display(needed)}},
4025        None{})
4026
4027def require_targets(targets: List<&2, String>,
4028    +requester: ArgInfo, +items: List<&2, SpecItem>,
4029    +fields: List<&2, Field>,
4030    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4031  match targets found:
4032    case Nil{} Some{fault}: Some{fault}
4033    case Nil{} None{}: None{}
4034    case name <> tail Some{fault}: Some{fault}
4035    case name <> tail None{}:
4036      require_targets(tail, requester, items, fields,
4037        require_target_fault(find_spec_info(items, name, None{}),
4038          requester, fields))
4039
4040def require_for_id(info: Maybe<&2, ArgInfo>,
4041    items: List<&2, SpecItem>,
4042    fields: List<&2, Field>) -> Maybe<&2, ScanFault>:
4043  match info:
4044    case None{}: None{}
4045    case Some{+requester}:
4046      require_targets(find_requires(info_attrs(requester)),
4047        requester, items, fields, None{})
4048
4049def require_go(ids: List<&2, String>,
4050    +items: List<&2, SpecItem>, +fields: List<&2, Field>,
4051    found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4052  match ids found:
4053    case Nil{} Some{fault}: Some{fault}
4054    case Nil{} None{}: None{}
4055    case id <> tail Some{fault}: Some{fault}
4056    case id <> tail None{}:
4057      require_go(tail, items, fields,
4058        require_for_id(find_spec_info(items, id, None{}), items, fields))
4059
4060def parse_requires(fault: Maybe<&2, ScanFault>,
4061    path: List<&2, String>, fields: List<&2, Field>,
4062    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4063  match fault:
4064    case Some{problem}: Rejected{path, problem}
4065    case None{}: Accepted{path, fields, external}
4066
4067def parse_conflicts(fault: Maybe<&2, ScanFault>,
4068    +scope: Scope, +fields: List<&2, Field>,
4069    events: List<&2, ScanEvent>,
4070    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4071  match fault:
4072    case Some{problem}: Rejected{scope_path(scope), problem}
4073    case None{}:
4074      parse_requires(require_go(
4075          present_order(events, fields),
4076          scope_arg_items(scope), fields, None{}),
4077        scope_path(scope), fields, external)
4078
4079def parse_missing(missing: List<&2, String>,
4080    +scope: Scope, +fields: List<&2, Field>,
4081    +events: List<&2, ScanEvent>,
4082    empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4083  match missing:
4084    case Nil{}:
4085      +items = scope_arg_items(scope)
4086      parse_conflicts(conflict_go(present_order(events, fields),
4087          [], items, exclusive_groups(items), None{}),
4088        scope, fields, events, external)
4089    case head <> tail:
4090      Bool.pick(ParseResult, empty, SummaryRequired{},
4091        Rejected{scope_path(scope), E06{head <> tail}})
4092
4093def parse_built(build: FieldBuild, +scope: Scope,
4094    events: List<&2, ScanEvent>, empty: Bool,
4095    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4096  match build:
4097    case BuildFault{fault}: Rejected{scope_path(scope), fault}
4098    case Built{+fields}:
4099      parse_missing(scope_missing(scope, fields),
4100        scope, fields, events, empty, external)
4101
4102def parse_no_issue(needs_sub: Bool, +scope: Scope,
4103    +events: List<&2, ScanEvent>, +h: Host,
4104    +empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4105  Bool.pick(ParseResult, needs_sub,
4106    Bool.pick(ParseResult, empty, SummaryRequired{},
4107      Rejected{scope_path(scope), E10{}}),
4108    parse_built(resolve_scope(scope, events, h),
4109      scope, events, empty, external))
4110
4111def parse_issue(issue: Maybe<&2, ScanFault>,
4112    +scope: Scope, events: List<&2, ScanEvent>,
4113    h: Host, empty: Bool,
4114    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4115  match issue:
4116    case Some{fault}: Rejected{scope_path(scope), fault}
4117    case None{}:
4118      parse_no_issue(scope_needs_sub(scope), scope,
4119        events, h, empty, external)
4120
4121def parse_early(found: Maybe<&2, Early>,
4122    +scope: Scope, events: List<&2, ScanEvent>, h: Host,
4123    empty: Bool, issue: Maybe<&2, ScanFault>,
4124    external: Maybe<&2, ExternalArgs>) -> ParseResult:
4125  match found:
4126    case Some{EarlyHelp{long}}:
4127      HelpRequested{scope_path(scope), long}
4128    case Some{EarlyVersion{}}:
4129      VersionRequested{}
4130    case None{}:
4131      parse_issue(issue, scope, events, h, empty, external)
4132
4133def parse_from_state(state: ScanState, h: Host,
4134    empty: Bool) -> ParseResult:
4135  match state:
4136    case ScanState{scope, events, resting, positional_count,
4137        external, issue}:
4138      +ordered: List<&2, ScanEvent> =
4139        List.reverse(&2, ScanEvent, events)
4140      parse_early(find_early(ordered), scope, ordered, h,
4141        empty, issue, external)
4142
4143def parse(c: Cmd, h: Host, +argv: List<&2, String>) -> ParseResult:
4144  parse_from_state(scan_close(scan_go(argv, Open{scan_start(c)})),
4145    h, List.is_empty(&2, String, argv))
4146
4147# ---- Reading values (§6.5) ----
4148
4149type Fault is Data:
4150  Bug{msg: String}
4151  Usage{msg: String}
4152
4153def Dec(-A: Type) -> Type:
4154  Result<&2, &1, Fault, A>
4155
4156def Dec.pure(-A: Type, x: A) -> Dec(A):
4157  Done{x}
4158
4159def Dec.bind(-A: Type, -B: Type, r: Dec(A), f: A -> Dec(B)) -> Dec(B):
4160  match r:
4161    case Fail{e}: Fail{e}
4162    case Done{x}: f(x)
4163
4164type Matches is Data:
4165  Matches{cmd: Cmd, path: List<&2, String>, fields: List<&2, Field>,
4166    external: Maybe<&2, ExternalArgs>}
4167
4168def matches(c: Cmd, r: ParseResult) -> Maybe<&2, Matches>:
4169  match r:
4170    case Accepted{path, fields, external}:
4171      Some{Matches{c, path, fields, external}}
4172    case _: None{}
4173
4174def path(m: Matches) -> List<&2, String>:
4175  match m:
4176    case Matches{cmd, path, fields, external}: path
4177
4178def external_args(m: Matches) -> Maybe<(String & List<&2, String>)>:
4179  match m:
4180    case Matches{cmd, path, fields, None{}}: None{}
4181    case Matches{cmd, path, fields, Some{ExternalArgs{name, args}}}:
4182      Some{(name, args)}
4183
4184def fail(-A: Type, msg: String) -> Dec(A):
4185  Fail{Usage{msg}}
4186
4187def bug(-A: Type, msg: String) -> Dec(A):
4188  Fail{Bug{msg}}
4189
4190def keep(+m: Matches) -> Dec(Matches):
4191  Done{m}
4192
4193# A command path as lint writes it: the root name, then each subcommand.
4194def path_text(name: String, path: List<&2, String>) -> String:
4195  String.join(name <> path, " ")
4196
4197def sub_items(found: Maybe<&2, SubFound>) -> List<&2, Item>:
4198  match found:
4199    case None{}: Nil{}
4200    case Some{SubFound{name, items}}: items
4201
4202# The args in scope at path (§8.1), as parse orders them: globals first.
4203def scope_args(path: List<&2, String>, +items: List<&2, Item>,
4204    globals: List<&2, Item>) -> List<&2, Item>:
4205  match path:
4206    case Nil{}: List.append(&2, Item, globals, items)
4207    case name <> tail:
4208      scope_args(tail, sub_items(find_sub(items, name)),
4209        List.append(&2, Item, global_items(items), globals))
4210
4211type Form is Data:
4212  ReadOne{}
4213  ReadOpt{}
4214  ReadMany{}
4215
4216# What a reader asks of an arg; Q-static checks it against the spec.
4217type Want is Data:
4218  WFlag{}
4219  WCount{}
4220  WValue{stem: String, form: Form}
4221  WAny{}
4222  WChoose{opt: Bool, keys: List<&2, String>}
4223
4224def kind_flag(kind: UsageKind) -> Bool:
4225  match kind:
4226    case FlagKind{}: True{}
4227    case NegatableKind{}: True{}
4228    case _: False{}
4229
4230def kind_count(kind: UsageKind) -> Bool:
4231  match kind:
4232    case CountKind{}: True{}
4233    case _: False{}
4234
4235# The reader name a value type reads with: str, nat, u32, f32 or bool.
4236def type_stem(found: Maybe<&2, ValueType>) -> String:
4237  match found:
4238    case Some{TNat{}}: "nat"
4239    case Some{TU32{}}: "u32"
4240    case Some{TF32{}}: "f32"
4241    case Some{TBool{}}: "bool"
4242    case _: "str"
4243
4244def stem_noun(+stem: String) -> String:
4245  Bool.pick(String, String.eq(stem, "str"), "string", stem)
4246
4247def stem_article(+stem: String) -> String:
4248  Bool.pick(String, String.eq(stem, "f32"), "an ", "a ")
4249
4250def always_present(kind: UsageKind, +attrs: List<&2, Item>) -> Bool:
4251  Bool.or(has_required(attrs), Bool.or(
4252    Maybe.is_some(&2, String, find_default(attrs)), kind_required_pos(kind)))
4253
4254def may_absent(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4255  Bool.and(Bool.not(kind_many(kind)),
4256    Bool.not(always_present(kind, attrs)))
4257
4258def kind_text(kind: UsageKind, +stem: String) -> String:
4259  match kind:
4260    case FlagKind{}: "a flag"
4261    case NegatableKind{}: "a flag"
4262    case CountKind{}: "a count"
4263    case _:
4264      Bool.pick(String, kind_many(kind),
4265        String.concat(["a list of ", stem_noun(stem), " values"]),
4266        String.concat([stem_article(stem), stem_noun(stem), " value"]))
4267
4268def fit_name(+kind: UsageKind, attrs: List<&2, Item>,
4269    +stem: String) -> String:
4270  match kind:
4271    case FlagKind{}: "flag"
4272    case NegatableKind{}: "flag"
4273    case CountKind{}: "count"
4274    case _:
4275      Bool.pick(String, kind_many(kind), String.append(stem, "s"),
4276        Bool.pick(String, always_present(kind, attrs), stem,
4277          String.append(stem, "_opt")))
4278
4279def kind_verdict(fits: Bool, id: String, +kind: UsageKind,
4280    +attrs: List<&2, Item>) -> Maybe<&2, String>:
4281  match fits:
4282    case True{}: None{}
4283    case False{}:
4284      +stem = type_stem(find_as(attrs))
4285      Some{String.concat(["'", id, "' is ", kind_text(kind, stem),
4286        "; read it with Cli.", fit_name(kind, attrs, stem)])}
4287
4288def absent_verdict(absent: Bool, id: String,
4289    reader: String) -> Maybe<&2, String>:
4290  match absent:
4291    case True{}:
4292      Some{String.concat(["'", id, "' may be absent; read it with Cli.",
4293        reader, "_opt"])}
4294    case False{}: None{}
4295
4296def all_in(xs: List<&2, String>, +ys: List<&2, String>) -> Bool:
4297  match xs:
4298    case Nil{}: True{}
4299    case head <> tail:
4300      Bool.and(List.contains(~String, ~String.eq, ys, head),
4301        all_in(tail, ys))
4302
4303def keys_issue(same: Bool, id: String) -> Maybe<&2, String>:
4304  match same:
4305    case True{}: None{}
4306    case False{}:
4307      Some{String.concat(["choose table for '", id,
4308        "' does not match its values"])}
4309
4310def keys_verdict(found: Maybe<&2, List<&2, String>>,
4311    +keys: List<&2, String>, id: String) -> Maybe<&2, String>:
4312  match found:
4313    case None{}: None{}
4314    case Some{+values}:
4315      keys_issue(Bool.and(Bool.not(List.is_empty(&2, String, keys)),
4316        Bool.and(all_in(keys, values), all_in(values, keys))), id)
4317
4318def form_fits(form: Form, many: Bool) -> Bool:
4319  match form:
4320    case ReadMany{}: many
4321    case _: Bool.not(many)
4322
4323def form_one(form: Form) -> Bool:
4324  match form:
4325    case ReadOne{}: True{}
4326    case _: False{}
4327
4328def value_fits(stem: String, form: Form, +kind: UsageKind,
4329    attrs: List<&2, Item>) -> Bool:
4330  Bool.and(kind_valued(kind), Bool.and(
4331    String.eq(stem, type_stem(find_as(attrs))),
4332    form_fits(form, kind_many(kind))))
4333
4334def choose_fits(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4335  Bool.and(kind_valued(kind), Bool.and(Bool.not(kind_many(kind)),
4336    Maybe.is_some(&2, List<&2, String>, find_one_of(attrs))))
4337
4338# The first Q-static message for an arg in scope: kind, absence, then keys.
4339def want_verdict(want: Want, +id: String, +kind: UsageKind,
4340    +attrs: List<&2, Item>) -> Maybe<&2, String>:
4341  match want:
4342    case WFlag{}:
4343      kind_verdict(kind_flag(kind), id, kind, attrs)
4344    case WCount{}:
4345      kind_verdict(kind_count(kind), id, kind, attrs)
4346    case WValue{+stem, +form}:
4347      Maybe.or(&2, String,
4348        kind_verdict(value_fits(stem, form, kind, attrs), id, kind, attrs),
4349        absent_verdict(Bool.and(form_one(form), may_absent(kind, attrs)),
4350          id, stem))
4351    case WAny{}:
4352      None{}
4353    case WChoose{opt, keys}:
4354      Maybe.or(&2, String,
4355        kind_verdict(choose_fits(kind, attrs), id, kind, attrs),
4356        Maybe.or(&2, String,
4357          absent_verdict(Bool.and(Bool.not(opt), may_absent(kind, attrs)),
4358            id, "choose"),
4359          keys_verdict(find_one_of(attrs), keys, id)))
4360
4361def arg_verdict(found: Maybe<&2, ArgInfo>, id: String, name: String,
4362    path: List<&2, String>, want: Want) -> Maybe<&2, String>:
4363  match found:
4364    case None{}:
4365      Some{String.concat(["'", id, "' is not an argument of '",
4366        path_text(name, path), "'"])}
4367    case Some{ArgInfo{key, usage, short, long, kind, attrs}}:
4368      want_verdict(want, id, kind, attrs)
4369
4370# Q-static: depends only on the spec, the path, the id and the want.
4371def verdict(cmd: Cmd, +path: List<&2, String>, +id: String,
4372    want: Want) -> Maybe<&2, String>:
4373  match cmd:
4374    case Cmd{name, about, items}:
4375      arg_verdict(find_arg_info(scope_args(path, items, []), id), id, name,
4376        path, want)
4377
4378def field_found(f: Field, id: String) -> Maybe<&2, Field>:
4379  match f:
4380    case Field{+key, values, source}:
4381      Bool.pick(Maybe<&2, Field>, same_text(key, id),
4382        Some{Field{key, values, source}}, None{})
4383
4384# The field for id, or an absent one; after a hit it walks no further.
4385def field_seek(fields: List<&2, Field>, +id: String,
4386    found: Maybe<&2, Field>) -> Field:
4387  match fields found:
4388    case _ Some{f}: f
4389    case Nil{} None{}: Field{id, [], Absent{}}
4390    case f <> tail None{}: field_seek(tail, id, field_found(f, id))
4391
4392def field_find(fields: List<&2, Field>, +id: String) -> Field:
4393  field_seek(fields, id, None{})
4394
4395# Validation first; reading a validated value never fails (§6.5).
4396def read_check(~T: Data, ~get: Field -> T, verdict: Maybe<&2, String>,
4397    fields: List<&2, Field>, id: String) -> Dec(T):
4398  match verdict:
4399    case None{}: Done{get(field_find(fields, id))}
4400    case Some{msg}: Fail{Bug{msg}}
4401
4402def read_with(~T: Data, ~get: Field -> T, want: Want, m: Matches,
4403    +id: String) -> Dec(T):
4404  match m:
4405    case Matches{cmd, path, fields, external}:
4406      read_check(~T, ~get, verdict(cmd, path, id, want), fields, id)
4407
4408def get_one(~T: Data, ~conv: String -> T, f: Field) -> T:
4409  match f:
4410    case Field{id, head <> tail, source}: conv(head)
4411    case _: conv("")
4412
4413def get_opt(~T: Data, ~conv: String -> T, f: Field) -> Maybe<&2, T>:
4414  match f:
4415    case Field{id, head <> tail, source}: Some{conv(head)}
4416    case _: None{}
4417
4418def convert_all(~T: Data, ~conv: String -> T, values: List<&2, String>) ->
4419    List<&2, T>:
4420  match values:
4421    case Nil{}: Nil{}
4422    case head <> tail: conv(head) <> convert_all(~T, ~conv, tail)
4423
4424def get_many(~T: Data, ~conv: String -> T, f: Field) -> List<&2, T>:
4425  match f:
4426    case Field{id, values, source}: convert_all(~T, ~conv, values)
4427
4428def true_of(s: String) -> Bool:
4429  String.eq(s, "true")
4430
4431def nat_of(s: String) -> Nat:
4432  Maybe.default(&2, Nat, Nat.read(s), 0n)
4433
4434def u32_of(s: String) -> U32:
4435  Maybe.default(&2, U32, U32.read(s), 0)
4436
4437def f32_of(s: String) -> F32:
4438  Maybe.default(&2, F32, F32.read(s), 0.0)
4439
4440def flag(m: Matches, id: String) -> Dec(Bool):
4441  read_with(~Bool, ~(f => get_one(~Bool, ~true_of, f)), WFlag{}, m, id)
4442
4443def count(m: Matches, id: String) -> Dec(Nat):
4444  read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4445    WCount{}, m, id)
4446
4447def str(m: Matches, id: String) -> Dec(String):
4448  read_with(~String, ~(f => get_one(~String, ~(s => s), f)),
4449    WValue{"str", ReadOne{}}, m, id)
4450
4451def str_opt(m: Matches, id: String) -> Dec(Maybe<&2, String>):
4452  read_with(~Maybe<&2, String>, ~(f => get_opt(~String, ~(s => s), f)),
4453    WValue{"str", ReadOpt{}}, m, id)
4454
4455def strs(m: Matches, id: String) -> Dec(List<&2, String>):
4456  read_with(~List<&2, String>, ~(f => get_many(~String, ~(s => s), f)),
4457    WValue{"str", ReadMany{}}, m, id)
4458
4459def nat(m: Matches, id: String) -> Dec(Nat):
4460  read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4461    WValue{"nat", ReadOne{}}, m, id)
4462
4463def nat_opt(m: Matches, id: String) -> Dec(Maybe<&2, Nat>):
4464  read_with(~Maybe<&2, Nat>, ~(f => get_opt(~Nat, ~nat_of, f)),
4465    WValue{"nat", ReadOpt{}}, m, id)
4466
4467def nats(m: Matches, id: String) -> Dec(List<&2, Nat>):
4468  read_with(~List<&2, Nat>, ~(f => get_many(~Nat, ~nat_of, f)),
4469    WValue{"nat", ReadMany{}}, m, id)
4470
4471def u32(m: Matches, id: String) -> Dec(U32):
4472  read_with(~U32, ~(f => get_one(~U32, ~u32_of, f)),
4473    WValue{"u32", ReadOne{}}, m, id)
4474
4475def u32_opt(m: Matches, id: String) -> Dec(Maybe<&2, U32>):
4476  read_with(~Maybe<&2, U32>, ~(f => get_opt(~U32, ~u32_of, f)),
4477    WValue{"u32", ReadOpt{}}, m, id)
4478
4479def u32s(m: Matches, id: String) -> Dec(List<&2, U32>):
4480  read_with(~List<&2, U32>, ~(f => get_many(~U32, ~u32_of, f)),
4481    WValue{"u32", ReadMany{}}, m, id)
4482
4483def f32(m: Matches, id: String) -> Dec(F32):
4484  read_with(~F32, ~(f => get_one(~F32, ~f32_of, f)),
4485    WValue{"f32", ReadOne{}}, m, id)
4486
4487def f32_opt(m: Matches, id: String) -> Dec(Maybe<&2, F32>):
4488  read_with(~Maybe<&2, F32>, ~(f => get_opt(~F32, ~f32_of, f)),
4489    WValue{"f32", ReadOpt{}}, m, id)
4490
4491def f32s(m: Matches, id: String) -> Dec(List<&2, F32>):
4492  read_with(~List<&2, F32>, ~(f => get_many(~F32, ~f32_of, f)),
4493    WValue{"f32", ReadMany{}}, m, id)
4494
4495def bool(m: Matches, id: String) -> Dec(Bool):
4496  read_with(~Bool, ~(f => get_one(~Bool, ~env_bool_true, f)),
4497    WValue{"bool", ReadOne{}}, m, id)
4498
4499def bool_opt(m: Matches, id: String) -> Dec(Maybe<&2, Bool>):
4500  read_with(~Maybe<&2, Bool>, ~(f => get_opt(~Bool, ~env_bool_true, f)),
4501    WValue{"bool", ReadOpt{}}, m, id)
4502
4503def bools(m: Matches, id: String) -> Dec(List<&2, Bool>):
4504  read_with(~List<&2, Bool>, ~(f => get_many(~Bool, ~env_bool_true, f)),
4505    WValue{"bool", ReadMany{}}, m, id)
4506
4507def field_src(f: Field) -> Src:
4508  match f:
4509    case Field{id, values, source}: source
4510
4511def given(m: Matches, id: String) -> Dec(Bool):
4512  read_with(~Bool, ~(f => source_given(field_src(f))), WAny{}, m, id)
4513
4514def source(m: Matches, id: String) -> Dec(Src):
4515  read_with(~Src, ~field_src, WAny{}, m, id)
4516
4517def by_key_hit(a, -A: Kind(a), same: Bool, value: A) -> Maybe<a, A>:
4518  match same:
4519    case True{}: Some{value}
4520    case False{}: None{}
4521
4522# The value under the first key equal to key; after a hit it walks no further.
4523def find_by_key_go(a, -A: Kind(a), keys: List<&2, String>,
4524    values: List<a, A>, +key: String, found: Maybe<a, A>) -> Maybe<a, A>:
4525  match keys values found:
4526    case _ _ Some{hit}: Some{hit}
4527    case head <> tail value <> rest None{}:
4528      find_by_key_go(a, A, tail, rest, key,
4529        by_key_hit(a, A, same_text(head, key), value))
4530    case _ _ None{}: None{}
4531
4532def find_by_key(a, -A: Kind(a), keys: List<&2, String>, values: List<a, A>,
4533    +key: String) -> Maybe<a, A>:
4534  find_by_key_go(a, A, keys, values, key, None{})
4535
4536def keyed_cons(a, -A: Kind(a), key: String, value: A,
4537    rest: (List<&2, String> & List<a, A>)) -> (List<&2, String> & List<a, A>):
4538  (keys, values) = rest
4539  (key <> keys, value <> values)
4540
4541# A choose or route table as its keys and its values.
4542def unzip_keyed(a, -A: Kind(a), table: List<(String & A)>) ->
4543    (List<&2, String> & List<a, A>):
4544  match table:
4545    case Nil{}: ([], [])
4546    case entry <> tail:
4547      (key, value) = entry
4548      keyed_cons(a, A, key, value, unzip_keyed(a, A, tail))
4549
4550# After validation the key exists: parse and samples keep one_of values.
4551def choice_dec(-A: Data, found: Maybe<&2, A>) -> Dec(Maybe<&2, A>):
4552  match found:
4553    case Some{value}: Done{Some{value}}
4554    case None{}: Fail{Usage{"the value has no choose entry"}}
4555
4556def choose_read(-A: Data, keys: List<&2, String>, values: List<&2, A>,
4557    f: Field) -> Dec(Maybe<&2, A>):
4558  match f:
4559    case Field{id, head <> tail, source}:
4560      choice_dec(A, find_by_key(&2, A, keys, values, head))
4561    case _: Done{None{}}
4562
4563def choose_check(-A: Data, verdict: Maybe<&2, String>,
4564    keys: List<&2, String>, values: List<&2, A>,
4565    fields: List<&2, Field>, id: String) -> Dec(Maybe<&2, A>):
4566  match verdict:
4567    case None{}: choose_read(A, keys, values, field_find(fields, id))
4568    case Some{msg}: Fail{Bug{msg}}
4569
4570def choose_pick(-A: Data, opt: Bool,
4571    choices: (List<&2, String> & List<&2, A>), m: Matches,
4572    +id: String) -> Dec(Maybe<&2, A>):
4573  (+keys, values) = choices
4574  match m:
4575    case Matches{cmd, path, fields, external}:
4576      choose_check(A, verdict(cmd, path, id, WChoose{opt, keys}),
4577        keys, values, fields, id)
4578
4579def choose_opt(-A: Data, m: Matches, id: String,
4580    table: List<(String & A)>) -> Dec(Maybe<&2, A>):
4581  choose_pick(A, True{}, unzip_keyed(&2, A, table), m, id)
4582
4583# After validation a bare choose always has a value.
4584def choice_must(-A: Data, d: Dec(Maybe<&2, A>)) -> Dec(A):
4585  match d:
4586    case Fail{e}: Fail{e}
4587    case Done{Some{value}}: Done{value}
4588    case Done{None{}}: Fail{Usage{"the value is absent"}}
4589
4590def choose(-A: Data, m: Matches, id: String,
4591    table: List<(String & A)>) -> Dec(A):
4592  choice_must(A, choose_pick(A, False{}, unzip_keyed(&2, A, table), m, id))
4593
4594def Call(-A: Type) -> Type:
4595  @+m: Matches -> Dec(A)
4596
4597# Some{None{}} when text is the word name, Some{Some{rest}} when it is name,
4598# a space and rest, None{} otherwise. Words are cut at spaces, so a name with
4599# a space (Chr{32}) is no word.
4600def word_rest(name: String, text: String, same: Bool) ->
4601    Maybe<&2, Maybe<&2, String>>:
4602  match name text same:
4603    case _ _ False{}: None{}
4604    case SNil{} SNil{} True{}: Some{None{}}
4605    case SNil{} SCon{Chr{32}, rest} True{}: Some{Some{rest}}
4606    case SCon{Chr{32}, xs} _ True{}: None{}
4607    case SCon{x, xs} SCon{y, ys} True{}: word_rest(xs, ys, Char.is_eq(x, y))
4608    case _ _ _: None{}
4609
4610# How a route key's next word meets an item: not its subcommand, the
4611# subcommand of a built-in, the command the key ends at, or a command whose
4612# subcommands take the rest (§6.4).
4613type KeyStep is Data:
4614  KeyMiss{}
4615  KeyStop{}
4616  KeyEnd{}
4617  KeyNext{rest: String}
4618
4619def key_user(builtin: Bool, rest: Maybe<&2, String>) -> KeyStep:
4620  match builtin rest:
4621    case True{} _: KeyStop{}
4622    case False{} None{}: KeyEnd{}
4623    case False{} Some{text}: KeyNext{text}
4624
4625def key_word(found: Maybe<&2, Maybe<&2, String>>,
4626    children: List<&2, Item>) -> KeyStep:
4627  match found:
4628    case None{}: KeyMiss{}
4629    case Some{rest}: key_user(has_builtin(children), rest)
4630
4631def key_step(items: List<&2, Item>, text: String) -> KeyStep:
4632  match items:
4633    case Sub{name, about, children} <> tail:
4634      key_word(word_rest(name, text, True{}), children)
4635    case _: KeyMiss{}
4636
4637# The first subcommand named by each word of text, from items down, is a
4638# command.
4639def key_go(items: List<&2, Item>, +text: String, step: KeyStep) -> Bool:
4640  match items step:
4641    case Nil{} _: False{}
4642    case head <> +tail KeyMiss{}: key_go(tail, text, key_step(tail, text))
4643    case head <> tail KeyStop{}: False{}
4644    case head <> tail KeyEnd{}: True{}
4645    case Sub{name, about, +children} <> tail KeyNext{+rest}:
4646      key_go(children, rest, key_step(children, rest))
4647    case _ _: False{}
4648
4649def route_text(empty: Bool, +key: String, +items: List<&2, Item>) -> Bool:
4650  match empty:
4651    case True{}: True{}
4652    case False{}: key_go(items, key, key_step(items, key))
4653
4654# A route key's words name a command; "" is the root.
4655def route_key(+key: String, items: List<&2, Item>) -> Bool:
4656  route_text(String.is_empty(key), key, items)
4657
4658def non_command(keys: List<&2, String>, +items: List<&2, Item>) ->
4659    Maybe<&2, String>:
4660  match keys:
4661    case Nil{}: None{}
4662    case +key <> tail:
4663      Bool.pick(Maybe<&2, String>, route_key(key, items),
4664        non_command(tail, items), Some{key})
4665
4666def route_call(-A: Type, bad: Maybe<&2, String>, call: Call(A),
4667    m: Matches) -> Dec(A):
4668  match bad:
4669    case None{}: call(m)
4670    case Some{key}:
4671      Fail{Bug{String.concat(["route '", key, "' is not a command"])}}
4672
4673# No route for the path is checked first, then every key names a command.
4674def route_found(-A: Type, found: Maybe<Call(A)>, keys: List<&2, String>,
4675    cmd: Cmd, path: List<&2, String>, m: Matches) -> Dec(A):
4676  match found cmd:
4677    case None{} Cmd{name, about, items}:
4678      Fail{Bug{String.concat(["no route for '", path_text(name, path), "'"])}}
4679    case Some{call} Cmd{name, about, items}:
4680      route_call(A, non_command(keys, items), call, m)
4681
4682def route_pick(-A: Type, routes: (List<&2, String> & List<Call(A)>),
4683    m: Matches) -> Dec(A):
4684  (+keys, calls) = routes
4685  match m:
4686    case Matches{+cmd, +path, fields, external}:
4687      route_found(A,
4688        find_by_key(&1, Call(A), keys, calls, String.join(path, " ")),
4689        keys, cmd, path, Matches{cmd, path, fields, external})
4690
4691def route(-A: Type, m: Matches, table: List<(String & Call(A))>) -> Dec(A):
4692  route_pick(A, unzip_keyed(&1, Call(A), table), m)
4693
4694# ---- Samples and check (§6.6) ----
4695
4696def sample_typed(found: Maybe<&2, ValueType>) -> String:
4697  match found:
4698    case Some{TNat{}}: "0"
4699    case Some{TU32{}}: "0"
4700    case Some{TF32{}}: "0.0"
4701    case Some{TBool{}}: "false"
4702    case _: "x"
4703
4704def sample_choice(found: Maybe<&2, List<&2, String>>,
4705    attrs: List<&2, Item>) -> String:
4706  match found:
4707    case Some{head <> tail}: head
4708    case _: sample_typed(find_as(attrs))
4709
4710def sample_value(kind: UsageKind, +attrs: List<&2, Item>) -> String:
4711  match kind:
4712    case FlagKind{}: "true"
4713    case NegatableKind{}: "true"
4714    case CountKind{}: "1"
4715    case _: sample_choice(find_one_of(attrs), attrs)
4716
4717def sample_field(build: FieldBuild, info: ArgInfo) -> List<&2, Field>:
4718  match build:
4719    case Built{Nil{}}:
4720      match info:
4721        case ArgInfo{id, usage, short, long, kind, attrs}:
4722          [Field{id, [sample_value(kind, attrs)], Argv{}}]
4723    case Built{fields}: fields
4724    case BuildFault{fault}: Nil{}
4725
4726# Each arg in scope: its default as parse resolves it, else a sample value.
4727def sample_args(items: List<&2, Item>) -> List<&2, Field>:
4728  match items:
4729    case Nil{}: Nil{}
4730    case Arg{usage, help, attrs} <> tail:
4731      +info = arg_info(usage, attrs)
4732      List.append(&2, Field,
4733        sample_field(resolve_default(info_default(info), info), info),
4734        sample_args(tail))
4735    case head <> tail: sample_args(tail)
4736
4737def sample_fields(c: Cmd, path: List<&2, String>) -> List<&2, Field>:
4738  match c:
4739    case Cmd{name, about, items}: sample_args(scope_args(path, items, []))
4740
4741def sample(+c: Cmd, +path: List<&2, String>) -> Matches:
4742  Matches{c, path, sample_fields(c, path), None{}}
4743
4744# The paths below prefix that can complete, in pre-order, without built-ins.
4745def sub_paths(items: List<&2, Item>, +prefix: List<&2, String>) ->
4746    List<&2, List<&2, String>>:
4747  match items:
4748    case Nil{}: Nil{}
4749    case Sub{name, about, +children} <> tail:
4750      +path = List.append(&2, String, prefix, [name])
4751      List.concat(&2, List<&2, String>, [
4752        Bool.pick(List<&2, List<&2, String>>,
4753          Bool.or(has_builtin(children), needs_sub(children)), [], [path]),
4754        sub_paths(children, path),
4755        sub_paths(tail, prefix),
4756      ])
4757    case head <> tail: sub_paths(tail, prefix)
4758
4759def sample_paths(c: Cmd) -> List<&2, List<&2, String>>:
4760  match c:
4761    case Cmd{name, about, +items}:
4762      List.append(&2, List<&2, String>,
4763        Bool.pick(List<&2, List<&2, String>>, needs_sub(items), [], [[]]),
4764        sub_paths(items, []))
4765
4766def decode_bug(-A: Type, d: Dec(A), at: String) -> List<&2, Problem>:
4767  match d:
4768    case Fail{Bug{msg}}: [DecodeBug{at, msg}]
4769    case _: Nil{}
4770
4771def cmd_name(c: Cmd) -> String:
4772  match c:
4773    case Cmd{name, about, items}: name
4774
4775def sample_bugs(~A: Type, ~decode: @+m: Matches -> Dec(A),
4776    paths: List<&2, List<&2, String>>, +c: Cmd) -> List<&2, Problem>:
4777  match paths:
4778    case Nil{}: Nil{}
4779    case +path <> tail:
4780      List.append(&2, Problem,
4781        decode_bug(A, decode(sample(c, path)), path_text(cmd_name(c), path)),
4782        sample_bugs(~A, ~decode, tail, c))
4783
4784def check(~A: Type, ~decode: @+m: Matches -> Dec(A), +c: Cmd) ->
4785    List<&2, Problem>:
4786  List.append(&2, Problem, problems(c),
4787    sample_bugs(~A, ~decode, sample_paths(c), c))
4788# ---- Color (§10.6) ----
4789
4790type Term is Data:
4791  Term{out_color: Bool, err_color: Bool, width: Nat}
4792
4793def host_tty(h: Host, out: Bool, err: Bool) -> Host:
4794  match h:
4795    case Host{width, vars, out_tty, err_tty}: Host{width, vars, out, err}
4796
4797# Rules 1 and 2 apply only to color() exactly as declared.
4798def color_attrs(attrs: List<&2, Item>) -> Bool:
4799  match attrs:
4800    case Con{OneOf{values}, Con{AttrDefault{value}, Con{Global{}, Nil{}}}}:
4801      Bool.and(String.eq(String.join(values, " "), "auto always never"),
4802        String.eq(value, "auto"))
4803    case _: False{}
4804
4805def is_color(item: Item) -> Bool:
4806  match item:
4807    case Arg{usage, help, attrs}:
4808      Bool.and(String.eq(usage, "--color <WHEN>"),
4809        Bool.and(String.eq(help, "When to use color"), color_attrs(attrs)))
4810    case _: False{}
4811
4812def has_color(items: List<&2, Item>) -> Bool:
4813  match items:
4814    case Nil{}: False{}
4815    case item <> tail: Bool.or(is_color(item), has_color(tail))
4816
4817# The pre-scan of argv: the last --color value before `--`, "" for a bare
4818# --color before a hyphen token.
4819type Prescan is Data:
4820  ColorSeek{last: Maybe<&2, String>}
4821  ColorWant{}
4822  ColorStop{last: Maybe<&2, String>}
4823
4824def color_seek(dash: Bool, bare: Bool, attached: Bool, token: String,
4825    last: Maybe<&2, String>) -> Prescan:
4826  match dash bare attached:
4827    case True{} _ _: ColorStop{last}
4828    case False{} True{} _: ColorWant{}
4829    case False{} False{} True{}: ColorSeek{Some{String.drop(token, 8n)}}
4830    case False{} False{} False{}: ColorSeek{last}
4831
4832def color_want(hyphen: Bool, +token: String) -> Prescan:
4833  match hyphen:
4834    case False{}: ColorSeek{Some{token}}
4835    case True{}:
4836      color_seek(String.eq(token, "--"), String.eq(token, "--color"),
4837        String.starts_with(token, "--color="), token, Some{""})
4838
4839def color_step(state: Prescan, +token: String) -> Prescan:
4840  match state:
4841    case ColorSeek{last}:
4842      color_seek(String.eq(token, "--"), String.eq(token, "--color"),
4843        String.starts_with(token, "--color="), token, last)
4844    case ColorWant{}: color_want(String.starts_with(token, "-"), token)
4845    case ColorStop{last}: ColorStop{last}
4846
4847def color_scan(argv: List<&2, String>, state: Prescan) -> Prescan:
4848  match argv:
4849    case Nil{}: state
4850    case token <> tail: color_scan(tail, color_step(state, token))
4851
4852def color_word(always: Bool, never: Bool) -> Maybe<&2, Bool>:
4853  match always never:
4854    case True{} _: Some{True{}}
4855    case False{} True{}: Some{False{}}
4856    case False{} False{}: None{}
4857
4858def color_value(found: Maybe<&2, String>) -> Maybe<&2, Bool>:
4859  match found:
4860    case None{}: None{}
4861    case Some{+value}:
4862      color_word(String.eq(value, "always"), String.eq(value, "never"))
4863
4864def color_pick(state: Prescan) -> Maybe<&2, Bool>:
4865  match state:
4866    case ColorSeek{last}: color_value(last)
4867    case ColorWant{}: None{}
4868    case ColorStop{last}: color_value(last)
4869
4870def color_flag(enabled: Bool, argv: List<&2, String>) -> Maybe<&2, Bool>:
4871  match enabled:
4872    case True{}: color_pick(color_scan(argv, ColorSeek{None{}}))
4873    case False{}: None{}
4874
4875# An empty variable counts as unset.
4876def var_set(found: Maybe<&2, String>) -> Bool:
4877  match found:
4878    case None{}: False{}
4879    case Some{value}: Bool.not(String.is_empty(value))
4880
4881def var_is(found: Maybe<&2, String>, text: String) -> Bool:
4882  match found:
4883    case None{}: False{}
4884    case Some{value}: String.eq(value, text)
4885
4886def force_set(found: Maybe<&2, String>) -> Bool:
4887  match found:
4888    case None{}: False{}
4889    case Some{+value}:
4890      Bool.and(Bool.not(String.is_empty(value)),
4891        Bool.not(String.eq(value, "0")))
4892
4893def env_off(off: Bool) -> Maybe<&2, Bool>:
4894  match off:
4895    case True{}: Some{False{}}
4896    case False{}: None{}
4897
4898def env_force(force: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
4899  match force:
4900    case True{}: Some{True{}}
4901    case False{}:
4902      env_off(Bool.or(var_is(var_value(vars, "CLICOLOR"), "0"),
4903        var_is(var_value(vars, "TERM"), "dumb")))
4904
4905def env_rules(no_color: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
4906  match no_color:
4907    case True{}: Some{False{}}
4908    case False{}:
4909      env_force(force_set(var_value(vars, "CLICOLOR_FORCE")), vars)
4910
4911# Rules 3-6; None leaves rule 7, each stream's TTY.
4912def env_color(+vars: List<&2, Var>) -> Maybe<&2, Bool>:
4913  env_rules(var_set(var_value(vars, "NO_COLOR")), vars)
4914
4915def term_env(found: Maybe<&2, Bool>, out_tty: Bool, err_tty: Bool,
4916    width: Nat) -> Term:
4917  match found:
4918    case Some{+on}: Term{on, on, width}
4919    case None{}: Term{out_tty, err_tty, width}
4920
4921def term_for(flag: Maybe<&2, Bool>, vars: List<&2, Var>, out_tty: Bool,
4922    err_tty: Bool, width: Nat) -> Term:
4923  match flag:
4924    case Some{+on}: Term{on, on, width}
4925    case None{}: term_env(env_color(vars), out_tty, err_tty, width)
4926
4927def term(c: Cmd, h: Host, argv: List<&2, String>) -> Term:
4928  match c h:
4929    case Cmd{name, about, items} Host{width, vars, out_tty, err_tty}:
4930      term_for(color_flag(has_color(items), argv), vars, out_tty, err_tty,
4931        width)
4932
4933# ---- Help (§10.2, §10.3) ----
4934
4935def plain(text: String) -> Segment:
4936  Segment{text, []}
4937
4938def page_width(width: Nat) -> Nat:
4939  Nat.min(100n, Nat.max(40n, width))
4940
4941# The first paragraph: the text before the first blank line.
4942def summary(s: String) -> String:
4943  match s:
4944    case SNil{}: SNil{}
4945    case SCon{Chr{10}, SCon{Chr{10}, tail}}: SNil{}
4946    case SCon{head, tail}: SCon{head, summary(tail)}
4947
4948def shown_text(long: Bool, text: String) -> String:
4949  match long:
4950    case True{}: text
4951    case False{}: summary(text)
4952
4953def filled(s: String) -> Bool:
4954  Bool.not(String.is_empty(s))
4955
4956def join_filled(parts: List<&2, String>) -> String:
4957  String.join(List.filter(~String, ~filled, parts), " ")
4958
4959# The command at the longest valid prefix of a path, with the globals it
4960# inherits, root first; stuck once a step names no subcommand.
4961type View is Data:
4962  View{path: String, about: String, items: List<&2, Item>,
4963    globals: List<&2, Item>, root: Bool, stuck: Bool}
4964
4965def find_command(items: List<&2, Item>, +query: String,
4966    found: Maybe<&2, Item>) -> Maybe<&2, Item>:
4967  match items found:
4968    case _ Some{item}: Some{item}
4969    case Nil{} None{}: None{}
4970    case Sub{+name, about, +children} <> tail None{}:
4971      find_command(tail, query, Bool.pick(Maybe<&2, Item>,
4972        Bool.or(String.eq(query, name),
4973          List.contains(~String, ~String.eq, find_aliases(children), query)),
4974        Some{Sub{name, about, children}}, None{}))
4975    case head <> tail None{}: find_command(tail, query, None{})
4976
4977def view_enter(found: Maybe<&2, Item>, path: String, about: String,
4978    items: List<&2, Item>, globals: List<&2, Item>, root: Bool) -> View:
4979  match found:
4980    case Some{Sub{name, sub_about, children}}:
4981      View{String.concat([path, " ", name]), sub_about, children,
4982        List.append(&2, Item, globals, global_items(items)), False{}, False{}}
4983    case _: View{path, about, items, globals, root, True{}}
4984
4985def view_step(view: View, step: String) -> View:
4986  match view:
4987    case View{path, about, items, globals, root, True{}}:
4988      View{path, about, items, globals, root, True{}}
4989    case View{path, about, +items, globals, root, False{}}:
4990      view_enter(find_command(items, step, None{}), path, about, items,
4991        globals, root)
4992
4993def view_walk(steps: List<&2, String>, view: View) -> View:
4994  match steps:
4995    case Nil{}: view
4996    case step <> tail: view_walk(tail, view_step(view, step))
4997
4998def view_of(c: Cmd, path: List<&2, String>) -> View:
4999  match c:
5000    case Cmd{name, about, items}:
5001      view_walk(path, View{name, about, items, [], True{}, False{}})
5002
5003def arg_pos(usage: String) -> Bool:
5004  usage_kind(~kind_pos, parse_usage(usage))
5005
5006def has_plain_option(items: List<&2, Item>) -> Bool:
5007  match items:
5008    case Nil{}: False{}
5009    case Arg{usage, help, +attrs} <> tail:
5010      Bool.or(Bool.and(Bool.not(arg_pos(usage)),
5011        Bool.and(Bool.not(has_hidden(attrs)), Bool.not(has_required(attrs)))),
5012        has_plain_option(tail))
5013    case head <> tail: has_plain_option(tail)
5014
5015def required_elem(required: Bool, usage: String, attrs: List<&2, Item>) ->
5016    List<&2, Segment>:
5017  match required:
5018    case True{}: [plain(arg_display(arg_info(usage, attrs)))]
5019    case False{}: []
5020
5021def required_options(items: List<&2, Item>) -> List<&2, Segment>:
5022  match items:
5023    case Nil{}: Nil{}
5024    case Arg{+usage, help, +attrs} <> tail:
5025      List.append(&2, Segment,
5026        required_elem(Bool.and(Bool.not(arg_pos(usage)), has_required(attrs)),
5027          usage, attrs),
5028        required_options(tail))
5029    case head <> tail: required_options(tail)
5030
5031def group_elem(listed: Bool, args: List<&2, Item>, group: String) ->
5032    List<&2, Segment>:
5033  match listed:
5034    case True{}: []
5035    case False{}: [plain(group_display(args, group))]
5036
5037# Each need_one group of items, over args, that done does not name yet.
5038def group_elems(items: List<&2, Item>, +args: List<&2, Item>,
5039    +done: List<&2, String>) -> List<&2, Segment>:
5040  match items:
5041    case Nil{}: Nil{}
5042    case NeedOne{+group} <> tail:
5043      List.append(&2, Segment,
5044        group_elem(List.contains(~String, ~String.eq, done, group), args,
5045          group),
5046        group_elems(tail, args, group <> done))
5047    case head <> tail: group_elems(tail, args, done)
5048
5049def positional_elems(items: List<&2, Item>) -> List<&2, Segment>:
5050  match items:
5051    case Nil{}: Nil{}
5052    case Arg{+usage, help, attrs} <> tail:
5053      +rest = positional_elems(tail)
5054      Bool.pick(List<&2, Segment>, arg_pos(usage), plain(usage) <> rest, rest)
5055    case head <> tail: positional_elems(tail)
5056
5057def command_elems(subs: Bool, need: Bool) -> List<&2, Segment>:
5058  match subs need:
5059    case False{} _: []
5060    case True{} True{}: [plain("<COMMAND>")]
5061    case True{} False{}: [plain("[COMMAND]")]
5062
5063def options_elem(present: Bool) -> List<&2, Segment>:
5064  match present:
5065    case True{}: [plain("[OPTIONS]")]
5066    case False{}: []
5067
5068# The path, [OPTIONS], required options, need_one groups as E06 writes them,
5069# positionals, then the subcommand slot.
5070def usage_elems(path: String, +items: List<&2, Item>,
5071    +globals: List<&2, Item>) -> List<&2, Segment>:
5072  +args = List.append(&2, Item, items, globals)
5073  +scope = List.append(&2, Item, globals, items)
5074  List.concat(&2, Segment, [
5075    [Segment{path, [Bold{}]}],
5076    options_elem(has_plain_option(args)),
5077    required_options(args),
5078    group_elems(scope, scope, []),
5079    positional_elems(items),
5080    command_elems(has_sub_item(items), needs_sub(items)),
5081  ])
5082
5083def segment_texts(segs: List<&2, Segment>) -> List<&2, String>:
5084  match segs:
5085    case Nil{}: Nil{}
5086    case Segment{text, styles} <> tail: text <> segment_texts(tail)
5087
5088def usage_view(view: View) -> String:
5089  match view:
5090    case View{path, about, items, globals, root, stuck}:
5091      String.join(segment_texts(usage_elems(path, items, globals)), " ")
5092
5093def usage(c: Cmd, path: List<&2, String>) -> String:
5094  usage_view(view_of(c, path))
5095
5096# Usage wraps between elements and continues at column 7.
5097def usage_rest(elems: List<&2, Segment>, +w: Nat, +col: Nat) ->
5098    List<&2, Segment>:
5099  match elems:
5100    case Nil{}: Nil{}
5101    case Segment{+text, styles} <> tail:
5102      +ew = width(text)
5103      +fits = Nat.is_le((col + 1n + ew : Nat), w)
5104      plain(Bool.pick(String, fits, " ", "\n       ")) <>
5105        Segment{text, styles} <> usage_rest(tail, w,
5106          Bool.pick(Nat, fits, (col + 1n + ew : Nat), (7n + ew : Nat)))
5107
5108def usage_block(elems: List<&2, Segment>, w: Nat) -> List<&2, Segment>:
5109  match elems:
5110    case Nil{}: [Segment{"Usage:", [Bold{}, Under{}]}]
5111    case Segment{+text, styles} <> tail:
5112      Segment{"Usage:", [Bold{}, Under{}]} <> plain(" ") <>
5113        Segment{text, styles} <> usage_rest(tail, w, (7n + width(text) : Nat))
5114
5115# A help row: its left cell, whether it is an option with a short or a
5116# long-only option, and its right text.
5117type Row is Data:
5118  Row{left: List<&2, Segment>, short: Bool, long_only: Bool, right: String}
5119
5120type Section is Data:
5121  Section{title: String, rows: List<&2, Row>}
5122
5123def tag(name: String, values: List<&2, String>) -> String:
5124  match values:
5125    case Nil{}: ""
5126    case head <> tail:
5127      String.concat(["[", name, ": ", String.join(head <> tail, ", "), "]"])
5128
5129def maybe_list(found: Maybe<&2, String>) -> List<&2, String>:
5130  match found:
5131    case None{}: Nil{}
5132    case Some{value}: [value]
5133
5134def list_or_none(found: Maybe<&2, List<&2, String>>) -> List<&2, String>:
5135  match found:
5136    case None{}: Nil{}
5137    case Some{values}: values
5138
5139def right_text(long: Bool, help: String, +attrs: List<&2, Item>) -> String:
5140  join_filled([
5141    shown_text(long, help),
5142    tag("env", find_envs(attrs)),
5143    tag("default", maybe_list(find_default(attrs))),
5144    tag("possible values", list_or_none(find_one_of(attrs))),
5145    tag("aliases", find_aliases(attrs)),
5146  ])
5147
5148def bold_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5149  match names:
5150    case Nil{}: Nil{}
5151    case name <> tail:
5152      List.append(&2, Segment,
5153        Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5154        Segment{String.trim_start(name), [Bold{}]} <> bold_names(tail, False{}))
5155
5156# Where an option's value or repeat starts: " <", "[=", or a final "...".
5157def value_cut(s: String) -> Nat:
5158  match s:
5159    case SNil{}: 0n
5160    case SCon{Chr{32}, SCon{Chr{60}, tail}}: 0n
5161    case SCon{Chr{91}, SCon{Chr{61}, tail}}: 0n
5162    case SCon{Chr{46}, SCon{Chr{46}, SCon{Chr{46}, SNil{}}}}: 0n
5163    case SCon{head, tail}: 1n+value_cut(tail)
5164
5165# Option names bold; separators, value and repeat plain.
5166def option_left(+usage: String) -> List<&2, Segment>:
5167  +cut = value_cut(usage)
5168  List.append(&2, Segment,
5169    bold_names(String.split(String.take(usage, cut), ','), True{}),
5170    [plain(String.drop(usage, cut))])
5171
5172def arg_row(parsed: Usage, usage: String, help: String,
5173    attrs: List<&2, Item>, long: Bool) -> Row:
5174  match parsed:
5175    case Parsed{id, +short, name, kind}:
5176      Row{option_left(usage), filled(short), String.is_empty(short),
5177        right_text(long, help, attrs)}
5178    case Invalid{col}:
5179      Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}
5180
5181def find_heading(attrs: List<&2, Item>) -> String:
5182  match attrs:
5183    case Nil{}: ""
5184    case Heading{title} <> tail: title
5185    case head <> tail: find_heading(tail)
5186
5187def option_row(shown: Bool, +usage: String, help: String,
5188    attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5189  match shown:
5190    case True{}: [arg_row(parse_usage(usage), usage, help, attrs, long)]
5191    case False{}: []
5192
5193# The visible options under a heading, "" for Options.
5194def option_rows(items: List<&2, Item>, +long: Bool, +heading: String) ->
5195    List<&2, Row>:
5196  match items:
5197    case Nil{}: Nil{}
5198    case Arg{+usage, help, +attrs} <> tail:
5199      List.append(&2, Row,
5200        option_row(Bool.and(Bool.not(arg_pos(usage)),
5201          Bool.and(Bool.not(has_hidden(attrs)),
5202            String.eq(find_heading(attrs), heading))),
5203          usage, help, attrs, long),
5204        option_rows(tail, long, heading))
5205    case head <> tail: option_rows(tail, long, heading)
5206
5207def positional_row(shown: Bool, usage: String, help: String,
5208    attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5209  match shown:
5210    case True{}:
5211      [Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}]
5212    case False{}: []
5213
5214def positional_rows(items: List<&2, Item>, +long: Bool) -> List<&2, Row>:
5215  match items:
5216    case Nil{}: Nil{}
5217    case Arg{+usage, help, +attrs} <> tail:
5218      List.append(&2, Row,
5219        positional_row(Bool.and(arg_pos(usage), Bool.not(has_hidden(attrs))),
5220          usage, help, attrs, long),
5221        positional_rows(tail, long))
5222    case head <> tail: positional_rows(tail, long)
5223
5224def command_row(hidden: Bool, name: String, about: String,
5225    children: List<&2, Item>) -> List<&2, Row>:
5226  match hidden:
5227    case True{}: []
5228    case False{}:
5229      [Row{[Segment{name, [Bold{}]}], False{}, False{},
5230        join_filled([summary(about), tag("aliases", find_aliases(children))])}]
5231
5232def command_rows(items: List<&2, Item>) -> List<&2, Row>:
5233  match items:
5234    case Nil{}: Nil{}
5235    case Sub{name, about, +children} <> tail:
5236      List.append(&2, Row,
5237        command_row(has_hidden(children), name, about, children),
5238        command_rows(tail))
5239    case head <> tail: command_rows(tail)
5240
5241def help_sub_row(present: Bool) -> List<&2, Row>:
5242  match present:
5243    case True{}:
5244      [Row{[Segment{"help", [Bold{}]}], False{}, False{},
5245        "Print this message or the help of the given subcommand(s)"}]
5246    case False{}: []
5247
5248# A built-in option row, without its short when an app arg took it.
5249def builtin_row(taken: Bool, short: String, long: String,
5250    text: String) -> Row:
5251  match taken:
5252    case True{}: Row{bold_names([long], True{}), False{}, True{}, text}
5253    case False{}:
5254      Row{bold_names([short, long], True{}), True{}, False{}, text}
5255
5256def help_text(differs: Bool, long: Bool) -> String:
5257  match differs long:
5258    case False{} _: "Print help"
5259    case True{} False{}: "Print help (see more with '--help')"
5260    case True{} True{}: "Print help (see a summary with '-h')"
5261
5262def version_row(present: Bool, args: List<&2, Item>) -> List<&2, Row>:
5263  match present:
5264    case True{}:
5265      [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-V")),
5266        "-V", "--version", "Print version")]
5267    case False{}: []
5268
5269# Headings of visible args, in order of first use.
5270def heading_titles(items: List<&2, Item>, +seen: List<&2, String>) ->
5271    List<&2, String>:
5272  match items:
5273    case Nil{}: seen
5274    case Arg{usage, help, +attrs} <> tail:
5275      +title = find_heading(attrs)
5276      heading_titles(tail, Bool.pick(List<&2, String>,
5277        Bool.or(String.is_empty(title), Bool.or(has_hidden(attrs),
5278          List.contains(~String, ~String.eq, seen, title))),
5279        seen, List.append(&2, String, seen, [title])))
5280    case head <> tail: heading_titles(tail, seen)
5281
5282def heading_sections(titles: List<&2, String>, +args: List<&2, Item>,
5283    +long: Bool) -> List<&2, Section>:
5284  match titles:
5285    case Nil{}: Nil{}
5286    case +title <> tail:
5287      Section{title, option_rows(args, long, title)} <>
5288        heading_sections(tail, args, long)
5289
5290# Whether a visible arg's help has more than one paragraph.
5291def args_paragraphs(items: List<&2, Item>) -> Bool:
5292  match items:
5293    case Nil{}: False{}
5294    case Arg{usage, help, attrs} <> tail:
5295      Bool.or(Bool.and(Bool.not(has_hidden(attrs)),
5296        String.contains(help, "\n\n")), args_paragraphs(tail))
5297    case head <> tail: args_paragraphs(tail)
5298
5299def indent_cell(indent: Bool, left: List<&2, Segment>) -> List<&2, Segment>:
5300  match indent:
5301    case True{}: plain("    ") <> left
5302    case False{}: left
5303
5304# A left cell; long-only options indent 4 more on a page with a short.
5305def row_cell(row: Row, shorted: Bool) -> List<&2, Segment>:
5306  match row:
5307    case Row{left, short, long_only, right}:
5308      indent_cell(Bool.and(long_only, shorted), left)
5309
5310def cell_width(cell: List<&2, Segment>) -> Nat:
5311  match cell:
5312    case Nil{}: 0n
5313    case Segment{text, styles} <> tail: Nat.add(width(text), cell_width(tail))
5314
5315def row_short(row: Row) -> Bool:
5316  match row:
5317    case Row{left, short, long_only, right}: short
5318
5319def any_short(rows: List<&2, Row>) -> Bool:
5320  match rows:
5321    case Nil{}: False{}
5322    case row <> tail: Bool.or(row_short(row), any_short(tail))
5323
5324# L, the widest left cell w with 2 + w + 2 <= K.
5325def widest(rows: List<&2, Row>, +shorted: Bool, +k: Nat, +best: Nat) -> Nat:
5326  match rows:
5327    case Nil{}: best
5328    case row <> tail:
5329      +w = cell_width(row_cell(row, shorted))
5330      widest(tail, shorted, k,
5331        Bool.pick(Nat, Nat.is_le((w + 4n : Nat), k), Nat.max(w, best), best))
5332
5333def pad_line(line: String, col: Nat) -> String:
5334  match line:
5335    case SNil{}: "\n"
5336    case SCon{head, tail}:
5337      String.concat(["\n", String.repeat(" ", col), SCon{head, tail}])
5338
5339def pad_lines(lines: List<&2, String>, +col: Nat) -> List<&2, Segment>:
5340  match lines:
5341    case Nil{}: Nil{}
5342    case line <> tail: plain(pad_line(line, col)) <> pad_lines(tail, col)
5343
5344def first_then(lines: List<&2, String>, col: Nat) -> List<&2, Segment>:
5345  match lines:
5346    case Nil{}: Nil{}
5347    case line <> tail: plain(line) <> pad_lines(tail, col)
5348
5349# A cell that fits takes its text after padding to C; one that does not
5350# stands alone, its text on the next lines at max(C, 10).
5351def row_form(fits: Bool, cell: List<&2, Segment>, lw: Nat, right: String,
5352    w: Nat, +c: Nat) -> List<&2, Segment>:
5353  match fits:
5354    case True{}:
5355      plain("  ") <> List.append(&2, Segment, cell,
5356        plain(String.repeat(" ", (c - 2n - lw : Nat))) <>
5357          first_then(String.lines(wrap(right, (w - c : Nat))), c))
5358    case False{}:
5359      +col = Nat.max(c, 10n)
5360      plain("  ") <> List.append(&2, Segment, cell,
5361        pad_lines(String.lines(wrap(right, (w - col : Nat))), col))
5362
5363def row_lines(row: Row, shorted: Bool, w: Nat, k: Nat, c: Nat) ->
5364    List<&2, Segment>:
5365  match row:
5366    case Row{left, short, long_only, right}:
5367      +cell = indent_cell(Bool.and(long_only, shorted), left)
5368      +lw = cell_width(cell)
5369      row_form(Nat.is_le((lw + 4n : Nat), k), cell, lw, right, w, c)
5370
5371def rows_lines(rows: List<&2, Row>, +shorted: Bool, +w: Nat, +k: Nat,
5372    +c: Nat) -> List<&2, Segment>:
5373  match rows:
5374    case Nil{}: Nil{}
5375    case row <> tail:
5376      plain("\n") <> List.append(&2, Segment, row_lines(row, shorted, w, k, c),
5377        rows_lines(tail, shorted, w, k, c))
5378
5379def section_rows(sections: List<&2, Section>) -> List<&2, Row>:
5380  match sections:
5381    case Nil{}: Nil{}
5382    case Section{title, rows} <> tail:
5383      List.append(&2, Row, rows, section_rows(tail))
5384
5385def section_blocks(sections: List<&2, Section>, +shorted: Bool, +w: Nat,
5386    +k: Nat, +c: Nat) -> List<&2, List<&2, Segment>>:
5387  match sections:
5388    case Nil{}: Nil{}
5389    case Section{title, Nil{}} <> tail: section_blocks(tail, shorted, w, k, c)
5390    case Section{title, rows} <> tail:
5391      (Segment{String.append(title, ":"), [Bold{}, Under{}]} <>
5392        rows_lines(rows, shorted, w, k, c)) <>
5393        section_blocks(tail, shorted, w, k, c)
5394
5395# One column for the page: C = L + 4, with K = 2W/5.
5396def sections_blocks(+sections: List<&2, Section>, +w: Nat) ->
5397    List<&2, List<&2, Segment>>:
5398  +rows = section_rows(sections)
5399  +shorted = any_short(rows)
5400  +k = Nat.div((2n * w : Nat), 5n)
5401  section_blocks(sections, shorted, w, k,
5402    (widest(rows, shorted, k, 0n) + 4n : Nat))
5403
5404def text_block(text: String, w: Nat) -> List<&2, List<&2, Segment>>:
5405  match text:
5406    case SNil{}: Nil{}
5407    case SCon{head, tail}: [[plain(wrap(SCon{head, tail}, w))]]
5408
5409def example_lines(items: List<&2, Item>) -> List<&2, Segment>:
5410  match items:
5411    case Nil{}: Nil{}
5412    case Example{line, what} <> tail:
5413      plain(String.concat(["\n  ", what, ":\n    $ ", line])) <>
5414        example_lines(tail)
5415    case head <> tail: example_lines(tail)
5416
5417def examples_block(lines: List<&2, Segment>) -> List<&2, List<&2, Segment>>:
5418  match lines:
5419    case Nil{}: Nil{}
5420    case head <> tail: [Segment{"Examples:", [Bold{}, Under{}]} <> head <> tail]
5421
5422def after_texts(items: List<&2, Item>) -> List<&2, String>:
5423  match items:
5424    case Nil{}: Nil{}
5425    case After{text} <> tail: text <> after_texts(tail)
5426    case head <> tail: after_texts(tail)
5427
5428def more_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5429  match blocks:
5430    case Nil{}: [plain("\n")]
5431    case block <> tail:
5432      plain("\n\n") <> List.append(&2, Segment, block, more_blocks(tail))
5433
5434# Blocks apart by one blank line; the page ends with a newline.
5435def join_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5436  match blocks:
5437    case Nil{}: Nil{}
5438    case block <> tail: List.append(&2, Segment, block, more_blocks(tail))
5439
5440def help_page(view: View, +long: Bool, +w: Nat) -> List<&2, Segment>:
5441  match view:
5442    case View{path, +about, +items, +globals, root, stuck}:
5443      +args = List.append(&2, Item, items, globals)
5444      join_blocks(List.concat(&2, List<&2, Segment>, [
5445        text_block(shown_text(long, about), w),
5446        [usage_block(usage_elems(path, items, globals), w)],
5447        sections_blocks(List.append(&2, Section, [
5448          Section{"Commands", List.append(&2, Row, command_rows(items),
5449            help_sub_row(has_sub_item(items)))},
5450          Section{"Arguments", positional_rows(items, long)},
5451          Section{"Options", List.concat(&2, Row, [
5452            option_rows(args, long, ""),
5453            [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-h")),
5454              "-h", "--help", help_text(Bool.or(String.contains(about, "\n\n"),
5455                args_paragraphs(args)), long))],
5456            version_row(Bool.and(root, has_version_item(items)), args),
5457          ])},
5458        ], heading_sections(heading_titles(args, []), args, long)), w),
5459        examples_block(example_lines(items)),
5460        text_block(String.join(after_texts(items), "\n\n"), w),
5461      ]))
5462
5463def help_out(t: Term, view: View, long: Bool) -> String:
5464  match t:
5465    case Term{out_color, err_color, width}:
5466      emit(out_color, help_page(view, long, page_width(width)))
5467
5468# The help page for the longest valid prefix of path, in stdout's colors.
5469def help(+c: Cmd, path: List<&2, String>, long: Bool, h: Host) -> String:
5470  help_out(term(c, h, []), view_of(c, path), long)
5471
5472# ---- Suggestions (§10.4) ----
5473
5474type Scored is Data:
5475  Scored{dist: Nat, name: String}
5476
5477def row_zero(b: String, +k: Nat) -> List<&2, Nat>:
5478  match b:
5479    case SNil{}: [k]
5480    case SCon{head, tail}: k <> row_zero(tail, (k + 1n : Nat))
5481
5482def head_nat(xs: List<&2, Nat>) -> Nat:
5483  match xs:
5484    case Nil{}: 0n
5485    case head <> tail: head
5486
5487def tail_nats(xs: List<&2, Nat>) -> List<&2, Nat>:
5488  match xs:
5489    case Nil{}: Nil{}
5490    case head <> tail: tail
5491
5492def same_char(found: Maybe<&2, Char>, c: Char) -> Bool:
5493  match found:
5494    case None{}: False{}
5495    case Some{d}: Char.is_eq(d, c)
5496
5497# Row i of optimal string alignment for a[i]: up starts at d[i-1][j-1], up2
5498# at d[i-2][j-2], and left is d[i][j-1].
5499def osa_row(bs: String, bp: Maybe<&2, Char>, +a: Char,
5500    +ap: Maybe<&2, Char>, +up: List<&2, Nat>, +up2: List<&2, Nat>,
5501    left: Nat) -> List<&2, Nat>:
5502  match bs:
5503    case SNil{}: Nil{}
5504    case SCon{+b, tail}:
5505      +diag = head_nat(up)
5506      +rest = tail_nats(up)
5507      +edit = Nat.min(Nat.min((head_nat(rest) + 1n : Nat), (left + 1n : Nat)),
5508        Bool.pick(Nat, Char.is_eq(a, b), diag, (diag + 1n : Nat)))
5509      +best = Bool.pick(Nat, Bool.and(same_char(bp, a), same_char(ap, b)),
5510        Nat.min(edit, (head_nat(up2) + 1n : Nat)), edit)
5511      best <> osa_row(tail, Some{b}, a, ap, rest, tail_nats(up2), best)
5512
5513def osa_rows(s: String, ap: Maybe<&2, Char>, +i: Nat, +b: String,
5514    +prev: List<&2, Nat>, prev2: List<&2, Nat>) -> List<&2, Nat>:
5515  match s:
5516    case SNil{}: prev
5517    case SCon{+a, tail}:
5518      osa_rows(tail, Some{a}, (i + 1n : Nat), b,
5519        i <> osa_row(b, None{}, a, ap, prev, 0n <> prev2, i), prev)
5520
5521def last_nat(xs: List<&2, Nat>, last: Nat) -> Nat:
5522  match xs:
5523    case Nil{}: last
5524    case head <> tail: last_nat(tail, head)
5525
5526def osa(a: String, +b: String) -> Nat:
5527  +first = row_zero(b, 0n)
5528  last_nat(osa_rows(a, None{}, 1n, b, first, first), 0n)
5529
5530def undashed(s: String) -> String:
5531  match s:
5532    case SCon{Chr{45}, tail}: undashed(tail)
5533    case _: s
5534
5535def scored(names: List<&2, String>, +typed: String, +limit: Nat) ->
5536    List<&2, Scored>:
5537  match names:
5538    case Nil{}: Nil{}
5539    case +name <> tail:
5540      +dist = osa(typed, String.take(name, 64n))
5541      +rest = scored(tail, typed, limit)
5542      Bool.pick(List<&2, Scored>, Nat.is_le(dist, limit),
5543        Scored{dist, name} <> rest, rest)
5544
5545def at_distance(found: List<&2, Scored>, +d: Nat) -> List<&2, String>:
5546  match found:
5547    case Nil{}: Nil{}
5548    case Scored{dist, name} <> tail:
5549      +rest = at_distance(tail, d)
5550      Bool.pick(List<&2, String>, Nat.is_eq(dist, d), name <> rest, rest)
5551
5552def by_distance(levels: Nat, +d: Nat, +found: List<&2, Scored>) ->
5553    List<&2, String>:
5554  match levels:
5555    case 0n: Nil{}
5556    case 1n+more:
5557      List.append(&2, String, at_distance(found, d),
5558        by_distance(more, (d + 1n : Nat), found))
5559
5560# At most 3 names within max(1, n / 3) edits, by distance, then order.
5561# key is bound because a temporary argument makes String.length take its
5562# string, adding a keep to each caller that reuses one (Nat.read.fits,
5563# arg_display).
5564def suggest(+typed: String, names: List<&2, String>) -> List<&2, String>:
5565  key = undashed(typed)
5566  +limit = Nat.max(1n, Nat.div(String.length(key), 3n))
5567  List.take(&2, String, by_distance((limit + 1n : Nat), 0n,
5568    scored(names, String.take(typed, 64n), limit)), 3n)
5569
5570# ---- Errors (§10.4) ----
5571
5572def quoted(text: String, styles: List<&2, Style>) -> List<&2, Segment>:
5573  [plain("'"), Segment{text, styles}, plain("'")]
5574
5575def missing_lines(missing: List<&2, String>) -> String:
5576  match missing:
5577    case Nil{}: ""
5578    case head <> tail: String.concat(["\n  ", head, missing_lines(tail)])
5579
5580def visible_subs(items: List<&2, Item>) -> List<&2, String>:
5581  match items:
5582    case Nil{}: ["help"]
5583    case Sub{name, about, children} <> tail:
5584      +rest = visible_subs(tail)
5585      Bool.pick(List<&2, String>, has_hidden(children), rest, name <> rest)
5586    case head <> tail: visible_subs(tail)
5587
5588def fault_message(fault: ScanFault, path: String,
5589    items: List<&2, Item>) -> List<&2, Segment>:
5590  match fault:
5591    case E01{token}:
5592      List.concat(&2, Segment, [[plain("unexpected argument ")],
5593        quoted(token, [Yellow{}]), [plain(" found")]])
5594    case E02{name}:
5595      plain("unrecognized subcommand ") <> quoted(name, [Yellow{}])
5596    case E03{arg}:
5597      [plain(String.concat(["a value is required for '", arg,
5598        "' but none was supplied"]))]
5599    case E04{arg, value, reason}:
5600      List.concat(&2, Segment, [[plain("invalid value ")],
5601        quoted(value, [Yellow{}]),
5602        [plain(String.concat([" for '", arg, "': ", reason]))]])
5603    case E05{arg, value}:
5604      List.concat(&2, Segment, [[plain("unexpected value ")],
5605        quoted(value, [Yellow{}]),
5606        [plain(String.concat([" for '", arg,
5607          "' found; no more were expected"]))]])
5608    case E06{missing}:
5609      [plain(String.append(
5610        "the following required arguments were not provided:",
5611        missing_lines(missing)))]
5612    case E07{a, b}:
5613      [plain(String.concat(["the argument '", a, "' cannot be used with '", b,
5614        "'"]))]
5615    case E08{a, b}:
5616      [plain(String.concat(["the argument '", a, "' requires '", b, "'"]))]
5617    case E09{token}:
5618      List.concat(&2, Segment, [[plain("unexpected argument ")],
5619        quoted(token, [Yellow{}]), [plain(" found")]])
5620    case E10{}:
5621      [plain(String.concat(["'", path,
5622        "' requires a subcommand but none was provided\n  [subcommands: ",
5623        String.join(visible_subs(items), ", "), "]"]))]
5624    case E11{arg, value, name, reason}:
5625      List.concat(&2, Segment, [[plain("invalid value ")],
5626        quoted(value, [Yellow{}]),
5627        [plain(String.concat([" for '", arg, "' from environment variable ",
5628          name, ": ", reason]))]])
5629
5630def green_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5631  match names:
5632    case Nil{}: Nil{}
5633    case name <> tail:
5634      List.concat(&2, Segment, [
5635        Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5636        quoted(name, [Green{}]), green_names(tail, False{})])
5637
5638def tip(text: String) -> List<&2, List<&2, Segment>>:
5639  [[plain(text)]]
5640
5641# T4: one suggestion or several.
5642def similar(kind: String, found: List<&2, String>) ->
5643    List<&2, List<&2, Segment>>:
5644  match found:
5645    case Nil{}: Nil{}
5646    case name <> Nil{}:
5647      [plain(String.concat(["a similar ", kind, " exists: "])) <>
5648        quoted(name, [Green{}])]
5649    case name <> more:
5650      [plain(String.concat(["some similar ", kind, "s exist: "])) <>
5651        green_names(name <> more, True{})]
5652
5653def long_name(s: String) -> Bool:
5654  String.starts_with(s, "--")
5655
5656def option_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
5657  match parsed:
5658    case Parsed{id, short, +long, kind}:
5659      List.concat(&2, String, [
5660        Bool.pick(List<&2, String>, String.is_empty(long), [], [long]),
5661        Bool.pick(List<&2, String>,
5662          Bool.and(kind_negatable(kind), filled(long)),
5663          [negated_long(long)], []),
5664        List.filter(~String, ~long_name, find_aliases(attrs)),
5665      ])
5666    case Invalid{col}: []
5667
5668def visible_names(hidden: Bool, usage: String, attrs: List<&2, Item>) ->
5669    List<&2, String>:
5670  match hidden:
5671    case True{}: []
5672    case False{}: option_names(parse_usage(usage), attrs)
5673
5674# T4 for E01: visible longs, their no- forms and long aliases.
5675def long_candidates(items: List<&2, Item>) -> List<&2, String>:
5676  match items:
5677    case Nil{}: Nil{}
5678    case Arg{usage, help, +attrs} <> tail:
5679      List.append(&2, String, visible_names(has_hidden(attrs), usage, attrs),
5680        long_candidates(tail))
5681    case head <> tail: long_candidates(tail)
5682
5683# T4 for E02: visible subcommand names and aliases, then help.
5684def sub_candidates(items: List<&2, Item>) -> List<&2, String>:
5685  match items:
5686    case Nil{}: ["help"]
5687    case Sub{name, about, +children} <> tail:
5688      List.append(&2, String,
5689        Bool.pick(List<&2, String>, has_hidden(children), [],
5690          name <> find_aliases(children)),
5691        sub_candidates(tail))
5692    case head <> tail: sub_candidates(tail)
5693
5694# T4 for E04: the one_of values of the arg E04 names.
5695def display_choices(items: List<&2, Item>, +arg: String,
5696    found: List<&2, String>) -> List<&2, String>:
5697  match items found:
5698    case _ head <> tail: head <> tail
5699    case Nil{} Nil{}: Nil{}
5700    case Arg{usage, help, +attrs} <> tail Nil{}:
5701      display_choices(tail, arg, Bool.pick(List<&2, String>,
5702        String.eq(arg_display(arg_info(usage, attrs)), arg),
5703        list_or_none(find_one_of(attrs)), []))
5704    case head <> tail Nil{}: display_choices(tail, arg, [])
5705
5706def owned(found: Maybe<&2, OptFound>, text: String) -> Maybe<&2, String>:
5707  match found:
5708    case None{}: None{}
5709    case Some{option}: Some{text}
5710
5711def owned_by(hit: Maybe<&2, OptFound>, token: String, prefix: String,
5712    step: String, found: Maybe<&2, String>) -> Maybe<&2, String>:
5713  match hit:
5714    case None{}: found
5715    case Some{option}:
5716      Some{String.concat(["'", token, "' belongs to '", prefix,
5717        "'; place it before '", step, "'"])}
5718
5719# T1: the deepest ancestor whose args take the long, and its child on path.
5720def owner(path: List<&2, String>, +items: List<&2, Item>, +prefix: String,
5721    +token: String, +long: String, found: Maybe<&2, String>) ->
5722    Maybe<&2, String>:
5723  match path:
5724    case Nil{}: found
5725    case +step <> tail:
5726      owner(tail, sub_items(find_sub(items, step)),
5727        String.concat([prefix, " ", step]), token, long,
5728        owned_by(find_option(items, long), token, prefix, step, found))
5729
5730# T2: the first subcommand in pre-order whose args take the long.
5731def descendant(items: List<&2, Item>, +prefix: String, +long: String,
5732    found: Maybe<&2, String>) -> Maybe<&2, String>:
5733  match items found:
5734    case _ Some{path}: Some{path}
5735    case Nil{} None{}: None{}
5736    case Sub{name, about, +children} <> tail None{}:
5737      +path = String.concat([prefix, " ", name])
5738      descendant(tail, prefix, long, descendant(children, path, long,
5739        owned(find_option(children, long), path)))
5740    case head <> tail None{}: descendant(tail, prefix, long, None{})
5741
5742def maybe_tip(found: Maybe<&2, String>) -> List<&2, List<&2, Segment>>:
5743  match found:
5744    case None{}: Nil{}
5745    case Some{text}: tip(text)
5746
5747def belongs(found: Maybe<&2, String>, token: String) ->
5748    List<&2, List<&2, Segment>>:
5749  match found:
5750    case None{}: Nil{}
5751    case Some{path}:
5752      tip(String.concat(["'", token, "' belongs to subcommand '", path, "'"]))
5753
5754def sub_exists(found: Maybe<&2, SubFound>, name: String) ->
5755    List<&2, List<&2, Segment>>:
5756  match found:
5757    case None{}: Nil{}
5758    case Some{sub}:
5759      tip(String.concat(["subcommand '", name,
5760        "' exists; to use it, remove the '--'"]))
5761
5762def before_eq(s: String) -> String:
5763  match s:
5764    case SNil{}: SNil{}
5765    case SCon{Chr{61}, tail}: SNil{}
5766    case SCon{head, tail}: SCon{head, before_eq(tail)}
5767
5768# T1, T2 and T3, for E01 on a long; --name=value looks up --name.
5769def long_tips(is_long: Bool, +token: String, root: String,
5770    path: List<&2, String>, +items: List<&2, Item>,
5771    root_items: List<&2, Item>, here: String) ->
5772    List<&2, List<&2, Segment>>:
5773  match is_long:
5774    case False{}: Nil{}
5775    case True{}:
5776      +long = before_eq(token)
5777      +name = String.drop(long, 2n)
5778      List.concat(&2, List<&2, Segment>, [
5779        maybe_tip(owner(path, root_items, root, token, long, None{})),
5780        belongs(descendant(items, here, long, None{}), token),
5781        sub_exists(find_sub(items, name), name),
5782      ])
5783
5784def operand_count(events: List<&2, ScanEvent>, count: Nat) -> Nat:
5785  match events:
5786    case Nil{}: count
5787    case Enter{name} <> tail: operand_count(tail, 0n)
5788    case Operand{token} <> tail: operand_count(tail, (count + 1n : Nat))
5789    case event <> tail: operand_count(tail, count)
5790
5791def trace_operands(trace: ScanTrace) -> Nat:
5792  match trace:
5793    case ScanTrace{path, events, ended, external, issue}:
5794      operand_count(events, 0n)
5795
5796def dash_slot(open: Bool, +token: String) -> List<&2, List<&2, Segment>>:
5797  match open:
5798    case False{}: Nil{}
5799    case True{}:
5800      tip(String.concat(["to pass '", token, "' as a value, use '-- ", token,
5801        "'"]))
5802
5803# T5: a hyphen token that the next positional slot could take after `--`;
5804# only then is argv scanned again to count the operands in scope.
5805def dash_tip(hyphen: Bool, token: String, items: List<&2, Item>, c: Cmd,
5806    argv: List<&2, String>) -> List<&2, List<&2, Segment>>:
5807  match hyphen:
5808    case False{}: Nil{}
5809    case True{}:
5810      dash_slot(positional_can_take(items, trace_operands(scan(c, argv))),
5811        token)
5812
5813def cmd_items(c: Cmd) -> List<&2, Item>:
5814  match c:
5815    case Cmd{name, about, items}: items
5816
5817# Every tip that applies, in table order.
5818def fault_tips(fault: ScanFault, +c: Cmd, +path: List<&2, String>,
5819    argv: List<&2, String>, here: String, +items: List<&2, Item>,
5820    globals: List<&2, Item>) -> List<&2, List<&2, Segment>>:
5821  match fault:
5822    case E01{+token}:
5823      List.concat(&2, List<&2, Segment>, [
5824        long_tips(long_name(token), token, cmd_name(c), path, items,
5825          cmd_items(c), here),
5826        similar("argument", suggest(token, List.concat(&2, String, [
5827          long_candidates(List.append(&2, Item, items, globals)),
5828          ["--help"],
5829          Bool.pick(List<&2, String>, Bool.and(List.is_empty(&2, String, path),
5830            has_version_item(items)), ["--version"], []),
5831        ]))),
5832        dash_tip(String.starts_with(token, "-"), token, items, c, argv),
5833      ])
5834    case E02{+name}: similar("subcommand", suggest(name, sub_candidates(items)))
5835    case E04{+arg, +value, reason}:
5836      similar("value", suggest(value,
5837        display_choices(List.append(&2, Item, items, globals), arg, [])))
5838    case E09{+token}:
5839      dash_tip(String.starts_with(token, "-"), token, items, c, argv)
5840    case _: Nil{}
5841
5842def tip_spans(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5843  match tips:
5844    case Nil{}: Nil{}
5845    case line <> tail:
5846      List.concat(&2, Segment, [
5847        [plain("  "), Segment{"tip:", [Bold{}, Green{}]}, plain(" ")],
5848        line, [plain("\n")], tip_spans(tail)])
5849
5850def tip_block(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5851  match tips:
5852    case Nil{}: Nil{}
5853    case head <> tail: plain("\n") <> tip_spans(head <> tail)
5854
5855def error_page(message: List<&2, Segment>,
5856    tips: List<&2, List<&2, Segment>>, usage: List<&2, Segment>) ->
5857    List<&2, Segment>:
5858  List.concat(&2, Segment, [
5859    [Segment{"error:", [Bold{}, Red{}]}, plain(" ")], message, [plain("\n")],
5860    tip_block(tips), [plain("\n")], usage,
5861    [plain("\n\nFor more information, try '"), Segment{"--help", [Bold{}]},
5862      plain("'.\n")],
5863  ])
5864
5865def fault_view(view: View, +fault: ScanFault, c: Cmd, path: List<&2, String>,
5866    argv: List<&2, String>, w: Nat) -> List<&2, Segment>:
5867  match view:
5868    case View{+here, about, +items, +globals, root, stuck}:
5869      error_page(fault_message(fault, here, items),
5870        fault_tips(fault, c, path, argv, here, items, globals),
5871        usage_block(usage_elems(here, items, globals), w))
5872
5873# An error at the scope where scanning stopped, in stderr's colors.
5874def fault_err(t: Term, fault: ScanFault, +c: Cmd, +path: List<&2, String>,
5875    argv: List<&2, String>) -> String:
5876  match t:
5877    case Term{out_color, err_color, width}:
5878      emit(err_color, fault_view(view_of(c, path), fault, c, path, argv,
5879        page_width(width)))
5880
5881def summary_err(t: Term, view: View) -> String:
5882  match t:
5883    case Term{out_color, err_color, width}:
5884      emit(err_color, help_page(view, False{}, page_width(width)))
5885
5886# ---- Lint lines (§9) ----
5887
5888def problem_line(p: Problem) -> String:
5889  match p:
5890    case BadUsage{at, usage, col}:
5891      String.concat(["  ", at, ": usage '", usage, "' is invalid at column ",
5892        Nat.show((col + 1n : Nat))])
5893    case BadName{at, name}:
5894      String.concat(["  ", at, ": '", name, "' is not a valid name"])
5895    case Dup{at, name}:
5896      String.concat(["  ", at, ": '", name, "' is declared more than once"])
5897    case Reserved{at, name}:
5898      String.concat(["  ", at, ": '", name, "' is reserved"])
5899    case Misplaced{at, attr}:
5900      String.concat(["  ", at, ": '", attr, "' is not allowed here"])
5901    case NoHelp{at, what}:
5902      String.concat(["  ", at, ": '", what, "' has no help text"])
5903    case BadDefault{at, id, value}:
5904      String.concat(["  ", at, ": '", id, "' cannot have the default '", value,
5905        "'"])
5906    case BadRef{at, id}:
5907      String.concat(["  ", at, ": '", id,
5908        "' is an unknown id or an unused or empty group"])
5909    case BadPositional{at, id, why}:
5910      String.concat(["  ", at, ": positional '", id, "': ", why])
5911    case Mixed{at}:
5912      String.concat(["  ", at, ": has both positionals and subcommands"])
5913    case BadChoices{at, id}:
5914      String.concat(["  ", at, ": one_of for '", id,
5915        "' is empty or repeats a value"])
5916    case BadChoice{at, id, value}:
5917      String.concat(["  ", at, ": '", id, "' cannot have the choice '", value,
5918        "'"])
5919    case NeedBare{at, id}:
5920      String.concat(["  ", at, ": '", id,
5921        "' needs [=<V>] and bare() together, with no short"])
5922    case DecodeBug{at, msg}:
5923      String.concat(["  ", at, ": decoder bug: ", msg])
5924
5925def problem_lines(found: List<&2, Problem>) -> String:
5926  match found:
5927    case Nil{}: ""
5928    case p <> tail: String.concat([problem_line(p), "\n", problem_lines(tail)])
5929
5930# ---- Completions and man pages (§11) ----
5931
5932type Shell is Data:
5933  Bash{}
5934  Zsh{}
5935  Fish{}
5936
5937def sh_body(s: String) -> String:
5938  match s:
5939    case SNil{}: SNil{}
5940    case SCon{Chr{39}, tail}: String.append("'\\''", sh_body(tail))
5941    case SCon{c, tail}: SCon{c, sh_body(tail)}
5942
5943# POSIX single quotes, with ' written as '\'' (P3).
5944def Sh.quote(s: String) -> String:
5945  SCon{'\'', String.append(sh_body(s), "'")}
5946
5947def fish_body(s: String) -> String:
5948  match s:
5949    case SNil{}: SNil{}
5950    case SCon{Chr{39}, tail}: String.append("\\'", fish_body(tail))
5951    case SCon{Chr{92}, tail}: String.append("\\\\", fish_body(tail))
5952    case SCon{c, tail}: SCon{c, fish_body(tail)}
5953
5954# fish single quotes, with \ and ' backslash-escaped (P4).
5955def Fish.quote(s: String) -> String:
5956  SCon{'\'', String.append(fish_body(s), "'")}
5957
5958# Text in an _arguments spec or a _describe entry, with \, [, ] and :
5959# backslash-escaped (P5).
5960def Zsh.esc(s: String) -> String:
5961  match s:
5962    case SNil{}: SNil{}
5963    case SCon{Chr{92}, tail}: String.append("\\\\", Zsh.esc(tail))
5964    case SCon{Chr{91}, tail}: String.append("\\[", Zsh.esc(tail))
5965    case SCon{Chr{93}, tail}: String.append("\\]", Zsh.esc(tail))
5966    case SCon{Chr{58}, tail}: String.append("\\:", Zsh.esc(tail))
5967    case SCon{c, tail}: SCon{c, Zsh.esc(tail)}
5968
5969# Text for man(7), start telling whether a line starts here: \, -, ", '
5970# and ` as escapes, and \& before a . or a space that starts a line.
5971def roff_body(s: String, start: Bool) -> String:
5972  match s start:
5973    case SNil{} _: SNil{}
5974    case SCon{Chr{46}, tail} True{}: String.append("\\&.", roff_body(tail, False{}))
5975    case SCon{Chr{32}, tail} True{}: String.append("\\& ", roff_body(tail, False{}))
5976    case SCon{Chr{10}, tail} _: SCon{'\n', roff_body(tail, True{})}
5977    case SCon{Chr{92}, tail} _: String.append("\\e", roff_body(tail, False{}))
5978    case SCon{Chr{45}, tail} _: String.append("\\-", roff_body(tail, False{}))
5979    case SCon{Chr{34}, tail} _: String.append("\\(dq", roff_body(tail, False{}))
5980    case SCon{Chr{39}, tail} _: String.append("\\(aq", roff_body(tail, False{}))
5981    case SCon{Chr{96}, tail} _: String.append("\\(ga", roff_body(tail, False{}))
5982    case SCon{c, tail} _: SCon{c, roff_body(tail, False{})}
5983
5984# Text that starts lines (P6, P15).
5985def Roff.esc(s: String) -> String:
5986  roff_body(s, True{})
5987
5988# Text after the start of a line.
5989def roff_text(s: String) -> String:
5990  roff_body(s, False{})
5991
5992def text_lines(lines: List<&2, String>) -> String:
5993  match lines:
5994    case Nil{}: ""
5995    case line <> tail: String.concat([line, "\n", text_lines(tail)])
5996
5997# The summary with its hard line breaks as spaces.
5998def one_line(s: String) -> String:
5999  String.join(String.lines(summary(s)), " ")
6000
6001def ident_char(+c: Char) -> Char:
6002  Bool.pick(Char, Bool.or(name_start(c), Char.is_eq(c, '_')), c, '_')
6003
6004# A name with every char outside [A-Za-z0-9_] as _, for shell identifiers.
6005def ident(s: String) -> String:
6006  match s:
6007    case SNil{}: SNil{}
6008    case SCon{c, tail}: SCon{ident_char(c), ident(tail)}
6009
6010def same_words(a: List<&2, String>, b: List<&2, String>) -> Bool:
6011  match a b:
6012    case Nil{} Nil{}: True{}
6013    case x <> xs y <> ys: Bool.and(String.eq(x, y), same_words(xs, ys))
6014    case _ _: False{}
6015
6016def last_with(lines: List<&2, String>, +suffix: String) -> List<&2, String>:
6017  match lines:
6018    case Nil{}: Nil{}
6019    case line <> Nil{}: [String.append(line, suffix)]
6020    case line <> tail: line <> last_with(tail, suffix)
6021
6022# A command on a visible path, in pre-order from the root: the words of its
6023# path below the root, its items, and the globals it inherits, root first.
6024type Place is Data:
6025  Place{words: List<&2, String>, items: List<&2, Item>,
6026    globals: List<&2, Item>}
6027
6028# The visible commands below items, whose subcommands inherit globals.
6029def places(items: List<&2, Item>, +words: List<&2, String>,
6030    +globals: List<&2, Item>) -> List<&2, Place>:
6031  match items:
6032    case Nil{}: Nil{}
6033    case Sub{name, about, +children} <> tail:
6034      +here = List.append(&2, String, words, [name])
6035      List.append(&2, Place,
6036        Bool.pick(List<&2, Place>, has_hidden(children), [],
6037          Place{here, children, globals} <> places(children, here,
6038            List.append(&2, Item, globals, global_items(children)))),
6039        places(tail, words, globals))
6040    case head <> tail: places(tail, words, globals)
6041
6042def all_places(+items: List<&2, Item>) -> List<&2, Place>:
6043  Place{[], items, []} <> places(items, [], global_items(items))
6044
6045# What a value completes to: a word of the one_of list numbered list, a
6046# file, a directory, or nothing.
6047type Comp is Data:
6048  Words{list: Nat, values: List<&2, String>}
6049  Files{}
6050  Dirs{}
6051  Free{}
6052
6053# An option as a script offers it: its names, shorts first, its kind, its
6054# placeholder, what its value completes to, and its help on one line.
6055type Opt is Data:
6056  Opt{names: List<&2, String>, kind: UsageKind, ph: String, comp: Comp,
6057    help: String}
6058
6059# A positional slot, numbered from 1.
6060type Slot is Data:
6061  Slot{k: Nat, hidden: Bool, required: Bool, many: Bool, rest: Bool,
6062    ph: String, comp: Comp}
6063
6064# A visible subcommand: its name, its names with aliases, and its summary.
6065type Entry is Data:
6066  Entry{name: String, names: List<&2, String>, about: String}
6067
6068# The number of values in lists, counting from k + 1 after a miss, or 0.
6069def list_number(lists: List<&2, List<&2, String>>,
6070    +values: List<&2, String>, hit: Bool, +k: Nat) -> Nat:
6071  match lists hit:
6072    case _ True{}: k
6073    case Nil{} False{}: 0n
6074    case head <> tail False{}:
6075      list_number(tail, values, same_words(head, values), (k + 1n : Nat))
6076
6077def add_list(found: Maybe<&2, List<&2, String>>,
6078    +lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6079  match found:
6080    case None{}: lists
6081    case Some{+values}:
6082      Bool.pick(List<&2, List<&2, String>>,
6083        List.contains(~List<&2, String>, ~same_words, lists, values), lists,
6084        List.append(&2, List<&2, String>, lists, [values]))
6085
6086def visible_one_of(hidden: Bool, attrs: List<&2, Item>) ->
6087    Maybe<&2, List<&2, String>>:
6088  match hidden:
6089    case True{}: None{}
6090    case False{}: find_one_of(attrs)
6091
6092def item_lists(items: List<&2, Item>, lists: List<&2, List<&2, String>>) ->
6093    List<&2, List<&2, String>>:
6094  match items:
6095    case Nil{}: lists
6096    case Arg{usage, help, +attrs} <> tail:
6097      item_lists(tail, add_list(visible_one_of(has_hidden(attrs), attrs), lists))
6098    case head <> tail: item_lists(tail, lists)
6099
6100# The one_of lists of visible args, each once, in pre-order.
6101def place_lists(places: List<&2, Place>,
6102    lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6103  match places:
6104    case Nil{}: lists
6105    case Place{words, items, globals} <> tail:
6106      place_lists(tail, item_lists(items, lists))
6107
6108def type_comp(found: Maybe<&2, ValueType>) -> Comp:
6109  match found:
6110    case Some{TFile{}}: Files{}
6111    case Some{TDir{}}: Dirs{}
6112    case _: Free{}
6113
6114def comp_of(found: Maybe<&2, List<&2, String>>, t: Maybe<&2, ValueType>,
6115    lists: List<&2, List<&2, String>>) -> Comp:
6116  match found:
6117    case Some{+values}: Words{list_number(lists, values, False{}, 0n), values}
6118    case None{}: type_comp(t)
6119
6120def head_part(parts: List<&2, String>) -> String:
6121  match parts:
6122    case head <> tail: head
6123    case Nil{}: ""
6124
6125def next_part(parts: List<&2, String>) -> String:
6126  match parts:
6127    case head <> next <> tail: next
6128    case _: ""
6129
6130def between(s: String, +open: Char, close: Char) -> String:
6131  head_part(String.split(next_part(String.split(s, open)), close))
6132
6133# The name in a usage's <>, or in an optional positional's [].
6134def placeholder(+usage: String) -> String:
6135  Bool.pick(String, String.contains(usage, "<"), between(usage, '<', '>'),
6136    between(usage, '[', ']'))
6137
6138def short_alias(s: String) -> Bool:
6139  Bool.not(long_name(s))
6140
6141def short_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
6142  match parsed:
6143    case Parsed{id, +short, long, kind}:
6144      List.append(&2, String,
6145        Bool.pick(List<&2, String>, String.is_empty(short), [], [short]),
6146        List.filter(~String, ~short_alias, find_aliases(attrs)))
6147    case Invalid{col}: []
6148
6149def parsed_kind(parsed: Usage) -> UsageKind:
6150  match parsed:
6151    case Parsed{id, short, long, kind}: kind
6152    case Invalid{col}: FlagKind{}
6153
6154def arg_opt(+parsed: Usage, usage: String, help: String,
6155    +attrs: List<&2, Item>, lists: List<&2, List<&2, String>>) -> Opt:
6156  Opt{List.append(&2, String, short_names(parsed, attrs),
6157      option_names(parsed, attrs)),
6158    parsed_kind(parsed), placeholder(usage),
6159    comp_of(find_one_of(attrs), find_as(attrs), lists), one_line(help)}
6160
6161# The visible options in items.
6162def arg_opts(items: List<&2, Item>, +lists: List<&2, List<&2, String>>) ->
6163    List<&2, Opt>:
6164  match items:
6165    case Nil{}: Nil{}
6166    case Arg{+usage, help, +attrs} <> tail:
6167      +rest = arg_opts(tail, lists)
6168      +parsed = parse_usage(usage)
6169      Bool.pick(List<&2, Opt>,
6170        Bool.or(usage_kind(~kind_pos, parsed), has_hidden(attrs)), rest,
6171        arg_opt(parsed, usage, help, attrs, lists) <> rest)
6172    case head <> tail: arg_opts(tail, lists)
6173
6174def flag_opt(names: List<&2, String>, help: String) -> Opt:
6175  Opt{names, FlagKind{}, "", Free{}, help}
6176
6177def version_opts(present: Bool, args: List<&2, Item>) -> List<&2, Opt>:
6178  match present:
6179    case True{}:
6180      [flag_opt(Bool.pick(List<&2, String>,
6181        Maybe.is_some(&2, OptFound, find_option(args, "-V")),
6182        ["--version"], ["-V", "--version"]), "Print version")]
6183    case False{}: []
6184
6185# -h and --help, without -h when an arg took it, then at a root with a
6186# version -V and --version likewise.
6187def builtin_opts(+items: List<&2, Item>, globals: List<&2, Item>,
6188    root: Bool) -> List<&2, Opt>:
6189  +args = List.append(&2, Item, items, globals)
6190  flag_opt(Bool.pick(List<&2, String>,
6191      Maybe.is_some(&2, OptFound, find_option(args, "-h")),
6192      ["--help"], ["-h", "--help"]), "Print help") <>
6193    version_opts(Bool.and(root, has_version_item(items)), args)
6194
6195def pos_slot(parsed: Usage, k: Nat, usage: String, +attrs: List<&2, Item>,
6196    lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6197  match parsed:
6198    case Parsed{id, short, long, PosKind{required, many}}:
6199      [Slot{k, has_hidden(attrs), required, many, has_rest(attrs),
6200        placeholder(usage), comp_of(find_one_of(attrs), find_as(attrs), lists)}]
6201    case _: []
6202
6203# The positional slots in items, hidden ones too, numbered from k.
6204def arg_slots(items: List<&2, Item>, +k: Nat,
6205    +lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6206  match items:
6207    case Nil{}: Nil{}
6208    case Arg{+usage, help, attrs} <> tail:
6209      +parsed = parse_usage(usage)
6210      List.append(&2, Slot, pos_slot(parsed, k, usage, attrs, lists),
6211        arg_slots(tail, Bool.pick(Nat, usage_kind(~kind_pos, parsed),
6212          (k + 1n : Nat), k), lists))
6213    case head <> tail: arg_slots(tail, k, lists)
6214
6215# The slots up to the variadic one, which takes every later value.
6216def completing(slots: List<&2, Slot>) -> List<&2, Slot>:
6217  match slots:
6218    case Nil{}: Nil{}
6219    case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6220      [Slot{k, hidden, required, True{}, rest, ph, comp}]
6221    case head <> tail: head <> completing(tail)
6222
6223def variadic(slots: List<&2, Slot>) -> Maybe<&2, Slot>:
6224  match slots:
6225    case Nil{}: None{}
6226    case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6227      Some{Slot{k, hidden, required, True{}, rest, ph, comp}}
6228    case head <> tail: variadic(tail)
6229
6230# The visible subcommands in items.
6231def sub_entries(items: List<&2, Item>) -> List<&2, Entry>:
6232  match items:
6233    case Nil{}: Nil{}
6234    case Sub{+name, about, +children} <> tail:
6235      +rest = sub_entries(tail)
6236      Bool.pick(List<&2, Entry>, has_hidden(children), rest,
6237        Entry{name, name <> find_aliases(children), one_line(about)} <> rest)
6238    case head <> tail: sub_entries(tail)
6239
6240def opt_names(opts: List<&2, Opt>) -> List<&2, String>:
6241  match opts:
6242    case Nil{}: Nil{}
6243    case Opt{names, kind, ph, comp, help} <> tail:
6244      List.append(&2, String, names, opt_names(tail))
6245
6246def entry_names(entries: List<&2, Entry>) -> List<&2, String>:
6247  match entries:
6248    case Nil{}: Nil{}
6249    case Entry{name, names, about} <> tail:
6250      List.append(&2, String, names, entry_names(tail))
6251
6252# What a script needs at a place: its number, its words, its options in
6253# scope, own first, the built-ins, its visible subcommands, and its slots.
6254type Stop is Data:
6255  Stop{k: Nat, words: List<&2, String>, opts: List<&2, Opt>,
6256    builtins: List<&2, Opt>, subs: List<&2, Entry>, slots: List<&2, Slot>}
6257
6258def stop_of(place: Place, k: Nat, +lists: List<&2, List<&2, String>>) ->
6259    Stop:
6260  match place:
6261    case Place{+words, +items, +globals}:
6262      Stop{k, words, List.append(&2, Opt, arg_opts(items, lists),
6263          arg_opts(globals, lists)),
6264        builtin_opts(items, globals, List.is_empty(&2, String, words)),
6265        sub_entries(items), arg_slots(items, 1n, lists)}
6266
6267def stops(places: List<&2, Place>, +k: Nat,
6268    +lists: List<&2, List<&2, String>>) -> List<&2, Stop>:
6269  match places:
6270    case Nil{}: Nil{}
6271    case place <> tail:
6272      stop_of(place, k, lists) <> stops(tail, (k + 1n : Nat), lists)
6273
6274# The number of the stop at words, from found on.
6275def stop_number(stops: List<&2, Stop>, +words: List<&2, String>,
6276    found: Maybe<&2, Nat>) -> Nat:
6277  match stops found:
6278    case _ Some{k}: k
6279    case Nil{} None{}: 0n
6280    case Stop{+k, here, opts, builtins, subs, slots} <> tail None{}:
6281      stop_number(tail, words, Bool.pick(Maybe<&2, Nat>,
6282        same_words(here, words), Some{k}, None{}))
6283
6284# ---- bash
6285
6286# Words quoted for sh, apart by sep.
6287def sh_list(words: List<&2, String>, +sep: String) -> String:
6288  match words:
6289    case Nil{}: ""
6290    case word <> Nil{}: Sh.quote(word)
6291    case word <> tail: String.concat([Sh.quote(word), sep, sh_list(tail, sep)])
6292
6293def dests(names: List<&2, String>, +dest: String) -> List<&2, String>:
6294  match names:
6295    case Nil{}: Nil{}
6296    case name <> tail: dest <> dests(tail, dest)
6297
6298# Each subcommand name's path, once per name.
6299def entry_dests(entries: List<&2, Entry>, +words: List<&2, String>) ->
6300    List<&2, String>:
6301  match entries:
6302    case Nil{}: Nil{}
6303    case Entry{name, names, about} <> tail:
6304      List.append(&2, String,
6305        dests(names, String.join(List.append(&2, String, words, [name]), " ")),
6306        entry_dests(tail, words))
6307
6308def bash_subs(+entries: List<&2, Entry>, words: List<&2, String>) ->
6309    List<&2, String>:
6310  Bool.pick(List<&2, String>, List.is_empty(&2, Entry, entries), [], [
6311    String.concat(["      subs=(", sh_list(entry_names(entries), " "), ")"]),
6312    String.concat(["      dests=(", sh_list(entry_dests(entries, words), " "),
6313      ")"]),
6314  ])
6315
6316def bash_shape(found: Maybe<&2, Slot>) -> List<&2, String>:
6317  match found:
6318    case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
6319      [String.concat(["      many=", Nat.show(k), " rest=",
6320        Bool.pick(String, rest, "1", "0")])]
6321    case None{}: []
6322
6323def bash_scope_arm(stop: Stop) -> List<&2, String>:
6324  match stop:
6325    case Stop{k, +words, opts, builtins, subs, slots}:
6326      String.concat(["    ", Sh.quote(String.join(words, " ")), ")"]) <>
6327        last_with(List.concat(&2, String, [
6328          [String.concat(["      opts=(", sh_list(List.append(&2, String,
6329            opt_names(opts), opt_names(builtins)), " "), ")"])],
6330          bash_subs(subs, words),
6331          bash_shape(variadic(slots)),
6332        ]), " ;;")
6333
6334def bash_letter(comp: Comp) -> String:
6335  match comp:
6336    case Words{list, values}: "w"
6337    case Files{}: "f"
6338    case Dirs{}: "d"
6339    case Free{}: "s"
6340
6341def bash_values(comp: Comp) -> String:
6342  match comp:
6343    case Words{list, values}: String.concat([" values=(", sh_list(values, " "), ")"])
6344    case _: ""
6345
6346def bash_kind(+comp: Comp, bare: Bool) -> String:
6347  String.concat(["kind=", bash_letter(comp),
6348    Bool.pick(String, bare, " opt=1", ""), bash_values(comp)])
6349
6350def bash_opt_case(opt: Opt) -> List<&2, String>:
6351  match opt:
6352    case Opt{names, OptionKind{many}, ph, comp, help}:
6353      [String.concat(["        ", sh_list(names, "|"), ") ",
6354        bash_kind(comp, False{}), " ;;"])]
6355    case Opt{names, BareOptionKind{}, ph, comp, help}:
6356      [String.concat(["        ", sh_list(names, "|"), ") ",
6357        bash_kind(comp, True{}), " ;;"])]
6358    case _: []
6359
6360def bash_opt_cases(opts: List<&2, Opt>) -> List<&2, String>:
6361  match opts:
6362    case Nil{}: Nil{}
6363    case opt <> tail: List.append(&2, String, bash_opt_case(opt), bash_opt_cases(tail))
6364
6365def bash_slot_cases(slots: List<&2, Slot>) -> List<&2, String>:
6366  match slots:
6367    case Nil{}: Nil{}
6368    case Slot{k, False{}, required, many, rest, ph, Words{list, values}} <> tail:
6369      String.concat(["        ", Nat.show(k), ") ",
6370        bash_kind(Words{list, values}, False{}), " ;;"]) <> bash_slot_cases(tail)
6371    case Slot{k, False{}, required, many, rest, ph, Files{}} <> tail:
6372      String.concat(["        ", Nat.show(k), ") kind=f ;;"]) <> bash_slot_cases(tail)
6373    case Slot{k, False{}, required, many, rest, ph, Dirs{}} <> tail:
6374      String.concat(["        ", Nat.show(k), ") kind=d ;;"]) <> bash_slot_cases(tail)
6375    case head <> tail: bash_slot_cases(tail)
6376
6377def bash_cases(words: List<&2, String>, cases: List<&2, String>) ->
6378    List<&2, String>:
6379  match cases:
6380    case Nil{}: []
6381    case head <> tail:
6382      List.concat(&2, String, [
6383        [String.concat(["    ", Sh.quote(String.join(words, " ")), ")"]),
6384          "      case $1 in"],
6385        head <> tail,
6386        ["      esac ;;"],
6387      ])
6388
6389def bash_arg_arm(stop: Stop) -> List<&2, String>:
6390  match stop:
6391    case Stop{k, words, opts, builtins, subs, slots}:
6392      bash_cases(words, List.append(&2, String, bash_opt_cases(opts),
6393        bash_slot_cases(completing(slots))))
6394
6395def bash_arms(~f: Stop -> List<&2, String>, stops: List<&2, Stop>) ->
6396    List<&2, String>:
6397  match stops:
6398    case Nil{}: Nil{}
6399    case stop <> tail: List.append(&2, String, f(stop), bash_arms(~f, tail))
6400# The rest of every bash script, for function prefix id and the quoted
6401# command name: the parser of COMP_WORDS, then the binding.
6402def bash_engine(+id: String, name: String) -> List<&2, String>:
6403  [
6404    "",
6405    "# Offers each value of kind that starts with $1, written after $2 and cut to",
6406    "# the part readline completes: the text after the last \"=\" or \":\". Readline",
6407    "# quotes files and directories; words are quoted here.",
6408    String.concat([id, "_offer() {"]),
6409    "  local word full i",
6410    "  case $kind in",
6411    "    f) compopt -o filenames; mapfile -t values < <(compgen -f -- \"$1\") ;;",
6412    "    d) compopt -o filenames; mapfile -t values < <(compgen -d -- \"$1\") ;;",
6413    "    w)",
6414    "      for ((i = 0; i < ${#values[@]}; i++)); do",
6415    "        printf -v 'values[i]' %q \"${values[i]}\"",
6416    "      done ;;",
6417    "    *) return ;;",
6418    "  esac",
6419    "  for word in \"${values[@]}\"; do",
6420    "    full=$2$word",
6421    "    [[ $word == \"$1\"* ]] && COMPREPLY+=(\"${full:${#2}+${#1}-${#tail}}\")",
6422    "  done",
6423    "}",
6424    "",
6425    String.concat([id, "() {"]),
6426    "  local -a words=() opts=() subs=() dests=() values=()",
6427    "  local cur tail word line gap path= want= kind opt many rest i j n=0 end=0 help=0",
6428    "  COMPREPLY=()",
6429    "  # Rejoin the \"=\" and \":\" that bash splits words at. COMP_LINE has no blank",
6430    "  # at such a split, so a \"=\" typed as its own word stays one.",
6431    "  line=${COMP_LINE#*\"${COMP_WORDS[0]}\"}",
6432    "  for ((i = 1; i <= COMP_CWORD; i++)); do",
6433    "    word=${COMP_WORDS[i]}",
6434    "    tail=${COMP_WORDS[i-1]}",
6435    "    gap=${line%%[![:blank:]]*}",
6436    "    line=${line:${#gap}+${#word}}",
6437    "    if ((i > 1)) && [[ -z $gap ]] &&",
6438    "      [[ -n $word && -z ${word//[=:]} || -n $tail && -z ${tail//[=:]} ]]; then",
6439    "      words[${#words[@]}-1]+=$word",
6440    "    else",
6441    "      words+=(\"$word\")",
6442    "    fi",
6443    "  done",
6444    "  tail=${COMP_WORDS[COMP_CWORD]}",
6445    "  [[ -z ${tail//[=:]} ]] && tail=",
6446    "  cur=${words[${#words[@]}-1]}",
6447    "  unset 'words[${#words[@]}-1]'",
6448    String.concat(["  ", id, "_scope"]),
6449    "  for word in \"${words[@]}\"; do",
6450    "    if [[ -n $want ]]; then",
6451    "      want=",
6452    "    elif ((!end)) && [[ $word == -- ]]; then",
6453    "      end=1",
6454    "    elif ((!end)) && [[ $word == --* ]]; then",
6455    String.concat(["      ", id, "_arg \"${word%%=*}\""]),
6456    "      [[ -n $kind && $opt == 0 && $word != *=* ]] && want=$word",
6457    "    elif ((!end)) && [[ $word == -?* ]]; then",
6458    "      for ((j = 1; j < ${#word}; j++)); do",
6459    String.concat(["        ", id, "_arg \"-${word:j:1}\""]),
6460    "        [[ -n $kind ]] || continue",
6461    "        ((j + 1 == ${#word})) && want=-${word:j:1}",
6462    "        break",
6463    "      done",
6464    "    elif ((${#subs[@]})); then",
6465    "      ((end)) && return",
6466    "      if [[ $word == help ]] && ((!help)); then",
6467    "        help=1",
6468    "        continue",
6469    "      fi",
6470    "      for ((j = 0; j < ${#subs[@]}; j++)); do",
6471    "        [[ $word == \"${subs[j]}\" ]] && break",
6472    "      done",
6473    "      ((j < ${#subs[@]})) || return",
6474    "      path=${dests[j]}",
6475    String.concat(["      ", id, "_scope"]),
6476    "    else",
6477    "      # n counts the slots filled. The variadic slot stays open, but a rest()",
6478    "      # value takes every later word, so nothing more completes.",
6479    "      n=$((n + 1))",
6480    "      if ((n == many && rest)); then",
6481    "        end=1",
6482    "      elif ((n == many)); then",
6483    "        n=$((n - 1))",
6484    "      fi",
6485    "    fi",
6486    "  done",
6487    "  if [[ -n $want ]]; then",
6488    String.concat(["    ", id, "_arg \"$want\""]),
6489    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6490    "  elif ((!end)) && [[ $cur == --*=* ]]; then",
6491    String.concat(["    ", id, "_arg \"${cur%%=*}\""]),
6492    String.concat(["    ", id, "_offer \"${cur#*=}\" \"${cur%%=*}=\""]),
6493    "  elif ((!end)) && [[ $cur == -* ]]; then",
6494    "    values=(\"${opts[@]}\") kind=w",
6495    "    ((help)) && values=('-h' '--help')",
6496    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6497    "  elif ((!end && ${#subs[@]})); then",
6498    "    values=(\"${subs[@]}\") kind=w",
6499    "    ((help)) || values+=('help')",
6500    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6501    "  elif ((!help)); then",
6502    String.concat(["    ", id, "_arg $((n + 1))"]),
6503    String.concat(["    ", id, "_offer \"$cur\" ''"]),
6504    "  fi",
6505    "}",
6506    "",
6507    String.concat(["complete -F ", id, " ", name]),
6508  ]
6509
6510def bash_script(+name: String, +items: List<&2, Item>) -> String:
6511  +all = all_places(items)
6512  +found = stops(all, 0n, place_lists(all, []))
6513  +id = String.append("_", ident(name))
6514  text_lines(List.concat(&2, String, [
6515    ["# bash completion; needs bash 4 or later.",
6516      "",
6517      "# Sets the tables of command path $path: opts, the options to offer; subs and",
6518      "# dests, each subcommand name and the path it enters; many, the slot of the",
6519      "# variadic positional, or 0; rest, 1 when that slot is rest().",
6520      String.append(id, "_scope() {"),
6521      "  opts=() subs=() dests=() many=0 rest=0",
6522      "  case $path in"],
6523    bash_arms(~bash_scope_arm, found),
6524    ["  esac",
6525      "}",
6526      "",
6527      "# Sets kind to what the option or positional $1 (a name, or a 1-based slot)",
6528      "# takes at $path: w, a word from values; f, a file; d, a directory; s, other",
6529      "# text. kind is empty for an option that takes no value and for a positional",
6530      "# that completes nothing; opt is 1 when the value can only follow \"=\".",
6531      String.append(id, "_arg() {"),
6532      "  kind= opt=0 values=()",
6533      "  case $path in"],
6534    bash_arms(~bash_arg_arm, found),
6535    ["  esac", "}"],
6536    bash_engine(id, Sh.quote(name)),
6537  ]))
6538
6539# ---- zsh
6540
6541def zsh_action(comp: Comp) -> String:
6542  match comp:
6543    case Words{list, values}: String.concat(["($v", Nat.show(list), ")"])
6544    case Files{}: "_files"
6545    case Dirs{}: "_files -/"
6546    case Free{}: " "
6547
6548# The prefix of an option's specs: * when it repeats, else its names as a
6549# group that excludes each other.
6550def zsh_pre(kind: UsageKind, +names: List<&2, String>) -> String:
6551  match kind:
6552    case CountKind{}: "*"
6553    case OptionKind{True{}}: "*"
6554    case _:
6555      Bool.pick(String, Nat.is_lt(1n, List.length(&2, String, names)),
6556        String.concat(["(", String.join(names, " "), ")"]), "")
6557
6558def zsh_spec(+name: String, kind: UsageKind, pre: String, d: String,
6559    ph: String, act: String) -> String:
6560  match kind:
6561    case OptionKind{many}:
6562      String.concat([pre, name, Bool.pick(String, long_name(name), "=", "+"),
6563        "[", d, "]:", ph, ":", act])
6564    case BareOptionKind{}:
6565      String.concat([pre, name, "=-[", d, "]::", ph, ":", act])
6566    case _: String.concat([pre, name, "[", d, "]"])
6567
6568def zsh_names(names: List<&2, String>, +kind: UsageKind, +pre: String,
6569    +d: String, +ph: String, +act: String) -> List<&2, String>:
6570  match names:
6571    case Nil{}: Nil{}
6572    case name <> tail:
6573      zsh_spec(name, kind, pre, d, ph, act) <>
6574        zsh_names(tail, kind, pre, d, ph, act)
6575
6576def zsh_opts(opts: List<&2, Opt>) -> List<&2, String>:
6577  match opts:
6578    case Nil{}: Nil{}
6579    case Opt{+names, +kind, ph, comp, help} <> tail:
6580      List.append(&2, String,
6581        zsh_names(names, kind, zsh_pre(kind, names), Zsh.esc(help),
6582          Zsh.esc(ph), zsh_action(comp)),
6583        zsh_opts(tail))
6584
6585def zsh_slots(slots: List<&2, Slot>) -> List<&2, String>:
6586  match slots:
6587    case Nil{}: Nil{}
6588    case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
6589      zsh_slots(tail)
6590    case Slot{k, False{}, required, True{}, False{}, ph, comp} <> tail:
6591      String.concat(["*:", Zsh.esc(ph), ":", zsh_action(comp)]) <>
6592        zsh_slots(tail)
6593    case Slot{k, False{}, required, +many, rest, ph, comp} <> tail:
6594      String.concat([Bool.pick(String, many, "(-)", ""), Nat.show(k),
6595        Bool.pick(String, required, ":", "::"), Zsh.esc(ph), ":",
6596        zsh_action(comp)]) <> zsh_slots(tail)
6597
6598# Specs quoted, one per line, all but the last continued.
6599def zsh_spec_lines(specs: List<&2, String>, +ind: String) ->
6600    List<&2, String>:
6601  match specs:
6602    case Nil{}: Nil{}
6603    case spec <> Nil{}: [String.concat([ind, "  ", Sh.quote(spec)])]
6604    case spec <> tail:
6605      String.concat([ind, "  ", Sh.quote(spec), " \\"]) <>
6606        zsh_spec_lines(tail, ind)
6607
6608def zsh_args(+ind: String, specs: List<&2, String>) -> List<&2, String>:
6609  String.append(ind, "_arguments -C -s -S \\") <> zsh_spec_lines(specs, ind)
6610
6611def zsh_entry(names: List<&2, String>, +ind: String, +about: String) ->
6612    List<&2, String>:
6613  match names:
6614    case Nil{}: Nil{}
6615    case name <> tail:
6616      String.concat([ind, "  ", Sh.quote(String.concat([Zsh.esc(name), ":",
6617        about]))]) <> zsh_entry(tail, ind, about)
6618
6619def zsh_entries(entries: List<&2, Entry>, +ind: String) -> List<&2, String>:
6620  match entries:
6621    case Nil{}: Nil{}
6622    case Entry{name, names, +about} <> tail:
6623      List.append(&2, String, zsh_entry(names, ind, Zsh.esc(about)),
6624        zsh_entries(tail, ind))
6625
6626def zsh_commands(entries: List<&2, Entry>, with_help: Bool, label: String,
6627    +ind: String) -> List<&2, String>:
6628  List.concat(&2, String, [
6629    [String.append(ind, "local -a commands=(")],
6630    zsh_entries(entries, ind),
6631    Bool.pick(List<&2, String>, with_help, [String.append(ind,
6632      "  'help:Print this message or the help of the given subcommand(s)'")],
6633      []),
6634    [String.append(ind, ")"),
6635      String.concat([ind, "_describe -t commands ", Sh.quote(label),
6636        " commands"])],
6637  ])
6638
6639def zsh_label(+name: String, words: List<&2, String>) -> String:
6640  String.append(String.join(name <> words, " "), " command")
6641
6642def zsh_deeper(arms: List<&2, String>, +depth: Nat, +ind: String) ->
6643    List<&2, String>:
6644  match arms:
6645    case Nil{}: []
6646    case head <> tail:
6647      List.concat(&2, String, [
6648        [String.append(ind, "else"),
6649          String.concat([ind, "  case $words[", Nat.show(depth), "] in"])],
6650        head <> tail,
6651        [String.append(ind, "  esac")],
6652      ])
6653
6654# After help at depth, the subcommand names, then deeper by arms.
6655def zsh_help_tree(entries: List<&2, Entry>, label: String,
6656    arms: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6657  List.concat(&2, String, [
6658    [String.concat([ind, "if (( CURRENT == ", Nat.show(depth), " )); then"])],
6659    zsh_commands(entries, False{}, label, String.append(ind, "  ")),
6660    zsh_deeper(arms, depth, ind),
6661    [String.append(ind, "fi")],
6662  ])
6663
6664def bar_names(names: List<&2, String>) -> String:
6665  match names:
6666    case Nil{}: ""
6667    case name <> Nil{}: Sh.quote(name)
6668    case name <> tail: String.concat([Sh.quote(name), "|", bar_names(tail)])
6669
6670# The help tree's arms at ind for the subcommands in items that have
6671# subcommands of their own.
6672def zsh_help_arms(items: List<&2, Item>, +name: String,
6673    +words: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6674  match items:
6675    case Nil{}: Nil{}
6676    case Sub{+sub, about, +children} <> tail:
6677      +here = List.append(&2, String, words, [sub])
6678      +entries = sub_entries(children)
6679      List.append(&2, String,
6680        Bool.pick(List<&2, String>,
6681          Bool.or(has_hidden(children), List.is_empty(&2, Entry, entries)), [],
6682          String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6683            last_with(zsh_help_tree(entries, zsh_label(name, here),
6684              zsh_help_arms(children, name, here, (depth + 1n : Nat),
6685                String.append(ind, "      ")),
6686              (depth + 1n : Nat), String.append(ind, "  ")), " ;;")),
6687        zsh_help_arms(tail, name, words, depth, ind))
6688    case head <> tail: zsh_help_arms(tail, name, words, depth, ind)
6689
6690# Ends a state that _arguments entered past a "--" it dropped, after which
6691# no subcommand follows.
6692def zsh_past_end(ind: String) -> String:
6693  String.append(ind, "(( ${#${(M)words[1,CURRENT-1]:#--}} < ends )) && return 1")
6694
6695# At the top command (no words), the count of "--" before the cursor that
6696# zsh_past_end compares with.
6697def zsh_ends(words: List<&2, String>, +ind: String) -> List<&2, String>:
6698  match words:
6699    case Nil{}:
6700      [String.append(ind,
6701        "# _arguments -S drops a -- typed before a command's first word, so a"),
6702        String.append(ind,
6703          "# state whose words hold fewer -- than the line has is past one."),
6704        String.append(ind, "local -i ends=${#${(M)words[2,CURRENT-1]:#--}}")]
6705    case _: []
6706
6707# The body of a command with subcommands: its specs end in a state that
6708# offers them, completes help's tree, or enters the arm of the one given.
6709def zsh_branch(+entries: List<&2, Entry>, specs: List<&2, String>,
6710    +items: List<&2, Item>, +name: String, +words: List<&2, String>,
6711    arms: List<&2, String>, +ind: String) -> List<&2, String>:
6712  +state = String.join(name <> words, "-")
6713  +i2 = String.append(ind, "    ")
6714  +hstate = String.append(state, "-help")
6715  +label = zsh_label(name, words)
6716  List.concat(&2, String, [
6717    zsh_ends(words, ind),
6718    last_with(zsh_args(ind, List.append(&2, String, specs,
6719      [String.append("*:: :->", state)])), " && return"),
6720    [String.append(ind, "case $state in"),
6721      String.concat([ind, "  ", Sh.quote(state), ")"]),
6722      zsh_past_end(i2),
6723      String.append(i2, "if (( CURRENT == 1 )); then")],
6724    zsh_commands(entries, True{}, label, String.append(i2, "  ")),
6725    [String.append(i2, "else"), String.append(i2, "  case $words[1] in")],
6726    arms,
6727    [String.append(i2, "    'help')")],
6728    last_with(zsh_args(String.append(i2, "      "), ["(-h --help)-h[Print help]",
6729      "(-h --help)--help[Print help]", String.append("*:: :->", hstate)]),
6730      " && return"),
6731    [String.append(i2, "      case $state in"),
6732      String.concat([i2, "        ", Sh.quote(hstate), ")"]),
6733      zsh_past_end(String.append(i2, "          "))],
6734    last_with(zsh_help_tree(entries, label,
6735      zsh_help_arms(items, name, words, 1n,
6736        String.append(i2, "              ")),
6737      1n, String.append(i2, "          ")), " ;;"),
6738    [String.append(i2, "      esac ;;"), String.append(i2, "  esac"),
6739      String.append(i2, "fi ;;"), String.append(ind, "esac")],
6740  ])
6741
6742def zsh_body(entries: List<&2, Entry>, specs: List<&2, String>,
6743    +items: List<&2, Item>, +lists: List<&2, List<&2, String>>,
6744    name: String, words: List<&2, String>, arms: List<&2, String>,
6745    ind: String) -> List<&2, String>:
6746  match entries:
6747    case Nil{}:
6748      zsh_args(ind, List.append(&2, String, specs,
6749        zsh_slots(completing(arg_slots(items, 1n, lists)))))
6750    case head <> tail:
6751      zsh_branch(head <> tail, specs, items, name, words, arms, ind)
6752
6753# The body at ind of the command with items and globals at words, given
6754# the arms of its subcommands.
6755def zsh_cmd(+items: List<&2, Item>, +globals: List<&2, Item>,
6756    +lists: List<&2, List<&2, String>>, name: String,
6757    +words: List<&2, String>, arms: List<&2, String>, ind: String) ->
6758    List<&2, String>:
6759  zsh_body(sub_entries(items),
6760    List.append(&2, String,
6761      zsh_opts(List.append(&2, Opt, arg_opts(items, lists),
6762        arg_opts(globals, lists))),
6763      zsh_opts(builtin_opts(items, globals,
6764        List.is_empty(&2, String, words)))),
6765    items, lists, name, words, arms, ind)
6766
6767# The arms at ind for the visible subcommands in items, which inherit
6768# globals; their bodies are 2 deeper.
6769def zsh_arms(items: List<&2, Item>, +globals: List<&2, Item>,
6770    +lists: List<&2, List<&2, String>>, +name: String,
6771    +words: List<&2, String>, +ind: String) -> List<&2, String>:
6772  match items:
6773    case Nil{}: Nil{}
6774    case Sub{+sub, about, +children} <> tail:
6775      +here = List.append(&2, String, words, [sub])
6776      List.append(&2, String,
6777        Bool.pick(List<&2, String>, has_hidden(children), [],
6778          String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6779            last_with(zsh_cmd(children, globals, lists, name, here,
6780              zsh_arms(children, List.append(&2, Item, globals,
6781                global_items(children)), lists, name, here,
6782                String.append(ind, "          ")),
6783              String.append(ind, "  ")), " ;;")),
6784        zsh_arms(tail, globals, lists, name, words, ind))
6785    case head <> tail: zsh_arms(tail, globals, lists, name, words, ind)
6786
6787def zsh_lists(lists: List<&2, List<&2, String>>, +k: Nat) ->
6788    List<&2, String>:
6789  match lists:
6790    case Nil{}: Nil{}
6791    case values <> tail:
6792      String.concat(["v", Nat.show(k), "=(", sh_list(values, " "), ")"]) <>
6793        zsh_lists(tail, (k + 1n : Nat))
6794
6795def zsh_arrays(lists: List<&2, List<&2, String>>) -> List<&2, String>:
6796  match lists:
6797    case Nil{}: []
6798    case head <> tail:
6799      [String.append("  local -a ", String.join(zsh_lists(head <> tail, 1n),
6800        " "))]
6801
6802def zsh_script(+name: String, +items: List<&2, Item>) -> String:
6803  +lists = place_lists(all_places(items), [])
6804  +id = String.append("_", ident(name))
6805  text_lines(List.concat(&2, String, [
6806    [String.append("#compdef ", name), "", String.append(id, "() {"),
6807      "  local curcontext=$curcontext state line"],
6808    zsh_arrays(lists),
6809    zsh_cmd(items, [], lists, name, [],
6810      zsh_arms(items, global_items(items), lists, name, [], "          "),
6811      "  "),
6812    ["}",
6813      "",
6814      String.concat(["if [[ $funcstack[1] == ", Sh.quote(String.append("_",
6815        name)), " ]]; then"]),
6816      String.concat(["  ", id, " \"$@\""]),
6817      "else",
6818      String.concat(["  compdef ", id, " ", Sh.quote(name)]),
6819      "fi"],
6820  ]))
6821
6822# ---- fish
6823
6824# Words quoted for fish, apart by spaces.
6825def fish_list(words: List<&2, String>) -> String:
6826  match words:
6827    case Nil{}: ""
6828    case word <> Nil{}: Fish.quote(word)
6829    case word <> tail: String.concat([Fish.quote(word), " ", fish_list(tail)])
6830
6831def fish_pats(names: List<&2, String>, +pre: String) -> List<&2, String>:
6832  match names:
6833    case Nil{}: Nil{}
6834    case name <> tail: Fish.quote(String.append(pre, name)) <> fish_pats(tail, pre)
6835
6836# The arms of _dest for subcommands entries of the path at words, numbered
6837# pre.
6838def fish_dest_arms(entries: List<&2, Entry>, +pre: String,
6839    +words: List<&2, String>, +found: List<&2, Stop>) -> List<&2, String>:
6840  match entries:
6841    case Nil{}: Nil{}
6842    case Entry{name, names, about} <> tail:
6843      String.append("        case ", String.join(fish_pats(names, pre), " ")) <>
6844        String.append("            echo ", Nat.show(stop_number(found,
6845          List.append(&2, String, words, [name]), None{}))) <>
6846        fish_dest_arms(tail, pre, words, found)
6847
6848def fish_dests(stops: List<&2, Stop>, +found: List<&2, Stop>) ->
6849    List<&2, String>:
6850  match stops:
6851    case Nil{}: Nil{}
6852    case Stop{k, words, opts, builtins, subs, slots} <> tail:
6853      List.append(&2, String,
6854        fish_dest_arms(subs, String.append(Nat.show(k), " "), words, found),
6855        fish_dests(tail, found))
6856
6857def valued_names(opts: List<&2, Opt>) -> List<&2, String>:
6858  match opts:
6859    case Nil{}: Nil{}
6860    case Opt{names, OptionKind{many}, ph, comp, help} <> tail:
6861      List.append(&2, String, names, valued_names(tail))
6862    case head <> tail: valued_names(tail)
6863
6864def fish_valued_arm(k: Nat, names: List<&2, String>) -> List<&2, String>:
6865  match names:
6866    case Nil{}: []
6867    case head <> tail:
6868      [String.append("        case ", Nat.show(k)),
6869        String.append("            contains -- $argv[2] ",
6870          fish_list(head <> tail))]
6871
6872def fish_valued(stop: Stop) -> List<&2, String>:
6873  match stop:
6874    case Stop{k, words, opts, builtins, subs, slots}:
6875      fish_valued_arm(k, valued_names(opts))
6876
6877def slot_shape(found: Maybe<&2, Slot>) -> String:
6878  match found:
6879    case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
6880      String.concat([Nat.show(k), " ", Bool.pick(String, rest, "1", "0")])
6881    case None{}: "0 0"
6882
6883def fish_shape_arm(shown: Bool, k: Nat, has: Bool, found: Maybe<&2, Slot>) ->
6884    List<&2, String>:
6885  match shown:
6886    case True{}:
6887      [String.append("        case ", Nat.show(k)),
6888        String.concat(["            echo ", Bool.pick(String, has, "1", "0"),
6889          " ", slot_shape(found)])]
6890    case False{}: []
6891
6892def fish_shape(stop: Stop) -> List<&2, String>:
6893  match stop:
6894    case Stop{k, words, opts, builtins, subs, slots}:
6895      +has = Bool.not(List.is_empty(&2, Entry, subs))
6896      +found = variadic(slots)
6897      fish_shape_arm(Bool.or(has, Maybe.is_some(&2, Slot, found)), k, has,
6898        found)
6899
6900def fish_values(lists: List<&2, List<&2, String>>, +k: Nat) ->
6901    List<&2, String>:
6902  match lists:
6903    case Nil{}: Nil{}
6904    case values <> tail:
6905      String.append("        case ", Nat.show(k)) <>
6906        String.append("            printf '%s\\n' ", fish_list(values)) <>
6907        fish_values(tail, (k + 1n : Nat))
6908
6909def fish_values_fn(+id: String, lists: List<&2, List<&2, String>>) ->
6910    List<&2, String>:
6911  match lists:
6912    case Nil{}: []
6913    case head <> tail:
6914      List.concat(&2, String, [
6915        ["", "# Prints the values of the one_of list numbered $argv[1].",
6916          String.concat(["function ", id, "_values"]), "    switch $argv[1]"],
6917        fish_values(head <> tail, 1n),
6918        ["    end", "end"],
6919      ])
6920
6921def fish_flag(long: Bool, name: String) -> String:
6922  match long:
6923    case True{}: String.append("-l ", Fish.quote(String.drop(name, 2n)))
6924    case False{}: String.append("-s ", Fish.quote(String.drop(name, 1n)))
6925
6926def fish_flags(names: List<&2, String>) -> List<&2, String>:
6927  match names:
6928    case Nil{}: Nil{}
6929    case +name <> tail: fish_flag(long_name(name), name) <> fish_flags(tail)
6930
6931# What completes a value comp offers: words, files, directories or none.
6932def fish_words(+id: String, comp: Comp) -> String:
6933  match comp:
6934    case Words{list, values}:
6935      String.concat([" -a '(", id, "_values ", Nat.show(list), ")'"])
6936    case Files{}: " -F"
6937    case Dirs{}: " -a '(__fish_complete_directories)'"
6938    case Free{}: ""
6939
6940def fish_needs(comp: Comp) -> String:
6941  match comp:
6942    case Files{}: " -r"
6943    case _: " -x"
6944
6945# An option of kind needs its value (-r), and only a file name may be any
6946# word (-x elsewhere); a [=<V>] value is optional.
6947def fish_kind(kind: UsageKind, +id: String, +comp: Comp) -> String:
6948  match kind:
6949    case OptionKind{many}:
6950      String.append(fish_needs(comp), fish_words(id, comp))
6951    case BareOptionKind{}: fish_words(id, comp)
6952    case _: ""
6953
6954def fish_opt_lines(opts: List<&2, Opt>, +at: String, +id: String) ->
6955    List<&2, String>:
6956  match opts:
6957    case Nil{}: Nil{}
6958    case Opt{names, kind, ph, comp, help} <> tail:
6959      String.concat([at, " n v' ", String.join(fish_flags(names), " "),
6960        fish_kind(kind, id, comp), " -d ", Fish.quote(help)]) <>
6961        fish_opt_lines(tail, at, id)
6962
6963# The built-ins, help first, which also holds after help (h).
6964def fish_builtin_lines(opts: List<&2, Opt>, +at: String, tags: String) ->
6965    List<&2, String>:
6966  match opts:
6967    case Nil{}: Nil{}
6968    case Opt{names, kind, ph, comp, help} <> tail:
6969      String.concat([at, tags, "' ", String.join(fish_flags(names), " "),
6970        " -d ", Fish.quote(help)]) <> fish_builtin_lines(tail, at, " n")
6971
6972def fish_name_lines(names: List<&2, String>, +at: String, +about: String) ->
6973    List<&2, String>:
6974  match names:
6975    case Nil{}: Nil{}
6976    case name <> tail:
6977      String.concat([at, " n h' -a ", Fish.quote(name), " -d ", about]) <>
6978        fish_name_lines(tail, at, about)
6979
6980def fish_sub_lines(entries: List<&2, Entry>, +at: String) -> List<&2, String>:
6981  match entries:
6982    case Nil{}: Nil{}
6983    case Entry{name, names, about} <> tail:
6984      List.append(&2, String, fish_name_lines(names, at, Fish.quote(about)),
6985        fish_sub_lines(tail, at))
6986
6987def fish_slot_lines(slots: List<&2, Slot>, +at: String, +id: String) ->
6988    List<&2, String>:
6989  match slots:
6990    case Nil{}: Nil{}
6991    case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
6992      fish_slot_lines(tail, at, id)
6993    case Slot{k, False{}, required, many, rest, ph, Free{}} <> tail:
6994      fish_slot_lines(tail, at, id)
6995    case Slot{k, False{}, required, many, rest, ph, comp} <> tail:
6996      String.concat([at, " p", Nat.show(k), "'", fish_words(id, comp)]) <>
6997        fish_slot_lines(tail, at, id)
6998
6999def fish_ends(subs: List<&2, Entry>, +at: String, id: String,
7000    slots: List<&2, Slot>) -> List<&2, String>:
7001  match subs:
7002    case Nil{}: fish_slot_lines(completing(slots), at, id)
7003    case head <> tail:
7004      List.append(&2, String, fish_sub_lines(head <> tail, at),
7005        [String.append(at, " n' -a 'help' -d 'Print this message or the help of the given subcommand(s)'")])
7006
7007def fish_lines(stops: List<&2, Stop>, +pre: String, +id: String) ->
7008    List<&2, String>:
7009  match stops:
7010    case Nil{}: Nil{}
7011    case Stop{k, words, opts, builtins, subs, slots} <> tail:
7012      +at = String.append(pre, Nat.show(k))
7013      List.concat(&2, String, [
7014        fish_opt_lines(opts, at, id),
7015        fish_builtin_lines(builtins, at, " n h"),
7016        fish_ends(subs, at, id, slots),
7017        fish_lines(tail, pre, id),
7018      ])
7019
7020# The function every fish script shares, for function prefix id: it walks
7021# the words before the cursor as the bash engine does.
7022def fish_engine(+id: String) -> List<&2, String>:
7023  [
7024    "",
7025    "# Succeeds when the words before the cursor reach path $argv[1] and the next",
7026    "# word can be one of $argv[2..-1]: n, an option or subcommand; h, a",
7027    "# subcommand after help; v, an option's value; p<k>, positional slot k.",
7028    String.concat(["function ", id, "_is"]),
7029    "    # fish 4 deprecates -o, which fish 3 needs.",
7030    "    set -l words (commandline -xpc 2>/dev/null; or commandline -opc)",
7031    "    set -e words[1]",
7032    "    set -l path 0",
7033    String.concat(["    set -l shape (", id, "_shape 0 | string split ' ')"]),
7034    "    set -l want 0",
7035    "    set -l end 0",
7036    "    set -l help 0",
7037    "    set -l n 0",
7038    "    for word in $words",
7039    "        if test $want = 1",
7040    "            set want 0",
7041    "        else if test $end = 0; and test \"$word\" = --",
7042    "            set end 1",
7043    "        else if test $end = 0; and string match -q -r -- '^--.' $word",
7044    String.concat(["            if not string match -q -- '*=*' $word; and ", id, "_valued $path $word"]),
7045    "                set want 1",
7046    "            end",
7047    "        else if test $end = 0; and string match -q -r -- '^-.' $word",
7048    "            set -l chars (string split '' -- (string sub -s 2 -- $word))",
7049    "            for i in (seq (count $chars))",
7050    String.concat(["                if ", id, "_valued $path -$chars[$i]"]),
7051    "                    test $i = (count $chars); and set want 1",
7052    "                    break",
7053    "                end",
7054    "            end",
7055    "        else if test $shape[1] = 1",
7056    "            if test $end = 1",
7057    "                return 1",
7058    "            else if test $help = 0; and test \"$word\" = help",
7059    "                set help 1",
7060    "            else",
7061    String.concat(["                set path (", id, "_dest $path $word); or return 1"]),
7062    String.concat(["                set shape (", id, "_shape $path | string split ' ')"]),
7063    "            end",
7064    "        else",
7065    "            # n counts the slots filled. The variadic slot stays open, but a",
7066    "            # rest() value takes every later word, so nothing more completes.",
7067    "            set n (math $n + 1)",
7068    "            if test $n = $shape[2]; and test $shape[3] = 1",
7069    "                set end 1",
7070    "            else if test $n = $shape[2]",
7071    "                set n (math $n - 1)",
7072    "            end",
7073    "        end",
7074    "    end",
7075    "    test $path = $argv[1]; or return 1",
7076    "    set -l tags",
7077    "    if test $want = 1",
7078    "        set tags v",
7079    "    else",
7080    "        test $end = 0; and test $help = 0; and set -a tags n",
7081    "        test $end = 0; and test $help = 1; and set -a tags h",
7082    "        test $shape[1] = 0; and test $help = 0; and set -a tags p(math $n + 1)",
7083    "    end",
7084    "    for tag in $argv[2..-1]",
7085    "        contains -- $tag $tags; and return 0",
7086    "    end",
7087    "    return 1",
7088    "end",
7089  ]
7090
7091def fish_script(+name: String, +items: List<&2, Item>) -> String:
7092  +all = all_places(items)
7093  +lists = place_lists(all, [])
7094  +found = stops(all, 0n, lists)
7095  +id = String.append("__", ident(name))
7096  +cq = Fish.quote(name)
7097  text_lines(List.concat(&2, String, [
7098    ["# fish completion; needs fish 3.3 or later.",
7099      "",
7100      "# Prints the number of the path that subcommand $argv[2] of path $argv[1]",
7101      "# enters; paths are numbered in pre-order from 0, the root.",
7102      String.concat(["function ", id, "_dest"]),
7103      "    switch \"$argv[1] $argv[2]\""],
7104    fish_dests(found, found),
7105    ["        case '*'", "            return 1", "    end", "end", "",
7106      "# Succeeds when option $argv[2] takes the next word as its value at path",
7107      "# $argv[1].",
7108      String.concat(["function ", id, "_valued"]),
7109      "    switch $argv[1]"],
7110    bash_arms(~fish_valued, found),
7111    ["        case '*'", "            return 1", "    end", "end", "",
7112      "# Prints how path $argv[1] takes words: 1 if it has subcommands, else 0; the",
7113      "# slot of its variadic positional, or 0; 1 if that slot is rest(), else 0.",
7114      String.concat(["function ", id, "_shape"]),
7115      "    switch $argv[1]"],
7116    bash_arms(~fish_shape, found),
7117    ["        case '*'", "            echo 0 0 0", "    end", "end"],
7118    fish_values_fn(id, lists),
7119    fish_engine(id),
7120    ["",
7121      "# Option lines also hold at v, so that fish can read grouped short options",
7122      "# before a value, as in -vC DIR.",
7123      String.concat(["complete -c ", cq, " -f"])],
7124    fish_lines(found, String.concat(["complete -c ", cq, " -n '", id, "_is "]),
7125      id),
7126  ]))
7127
7128# ---- man pages
7129
7130def roff_bold(s: String) -> String:
7131  String.concat(["\\fB", roff_text(s), "\\fR"])
7132
7133def roff_bolds(names: List<&2, String>) -> List<&2, String>:
7134  match names:
7135    case Nil{}: Nil{}
7136    case name <> tail: roff_bold(String.trim_start(name)) <> roff_bolds(tail)
7137
7138def roff_texts(texts: List<&2, String>) -> List<&2, String>:
7139  match texts:
7140    case Nil{}: Nil{}
7141    case text <> tail: roff_text(text) <> roff_texts(tail)
7142
7143def spaces_only(s: String) -> Bool:
7144  match s:
7145    case SNil{}: True{}
7146    case SCon{Chr{32}, tail}: spaces_only(tail)
7147    case SCon{head, tail}: False{}
7148
7149def line_mark(blank: Bool, line: String) -> List<&2, Maybe<&2, String>>:
7150  match blank:
7151    case True{}: []
7152    case False{}: [Some{line}]
7153
7154# Each line, None for an empty one, which ends a paragraph; a line of only
7155# spaces is left out.
7156def para_marks(lines: List<&2, String>) -> List<&2, Maybe<&2, String>>:
7157  match lines:
7158    case Nil{}: Nil{}
7159    case SNil{} <> tail: None{} <> para_marks(tail)
7160    case +line <> tail:
7161      List.append(&2, Maybe<&2, String>, line_mark(spaces_only(line), line),
7162        para_marks(tail))
7163
7164# Marked lines escaped, after gap: nothing before the first, .br within a
7165# paragraph, sep between paragraphs.
7166def roff_marks(marks: List<&2, Maybe<&2, String>>, +sep: String,
7167    gap: Maybe<&2, String>) -> List<&2, String>:
7168  match marks gap:
7169    case Nil{} _: Nil{}
7170    case None{} <> tail None{}: roff_marks(tail, sep, None{})
7171    case None{} <> tail Some{g}: roff_marks(tail, sep, Some{sep})
7172    case Some{line} <> tail None{}:
7173      Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7174    case Some{line} <> tail Some{g}:
7175      g <> Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7176
7177def roff_paras(text: String, sep: String) -> List<&2, String>:
7178  roff_marks(para_marks(String.lines(text)), sep, None{})
7179
7180def ph_end(s: String) -> Nat:
7181  match s:
7182    case SNil{}: 0n
7183    case SCon{Chr{62}, tail}: 0n
7184    case SCon{head, tail}: 1n+ph_end(tail)
7185
7186# A placeholder's name in italics, from after its <.
7187def roff_ph(+s: String) -> String:
7188  +end = ph_end(s)
7189  String.concat([roff_text(String.take(s, end)), ">\\fR",
7190    roff_text(String.drop(s, (end + 1n : Nat)))])
7191
7192def roff_value(rest: String) -> String:
7193  match rest:
7194    case SCon{Chr{32}, SCon{Chr{60}, tail}}: String.append(" \\fI<", roff_ph(tail))
7195    case SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, tail}}}:
7196      String.append("[=\\fI<", roff_ph(tail))
7197    case SNil{}: ""
7198    case SCon{head, tail}: roff_text(SCon{head, tail})
7199
7200# Option names bold, the placeholder in italics.
7201def roff_option(+usage: String) -> String:
7202  +cut = value_cut(usage)
7203  String.append(String.join(roff_bolds(String.split(String.take(usage, cut),
7204    ',')), ", "), roff_value(String.drop(usage, cut)))
7205
7206# A positional in italics, apart from its repeat.
7207def roff_positional(+usage: String) -> String:
7208  +cut = value_cut(usage)
7209  String.concat(["\\fI", roff_text(String.take(usage, cut)), "\\fR",
7210    roff_text(String.drop(usage, cut))])
7211
7212def roff_entry(left: String, help: String, attrs: List<&2, Item>) ->
7213    List<&2, String>:
7214  ".TP" <> left <> roff_paras(right_text(True{}, help, attrs), ".IP")
7215
7216def roff_positional_entry(shown: Bool, usage: String, help: String,
7217    attrs: List<&2, Item>) -> List<&2, String>:
7218  match shown:
7219    case True{}: roff_entry(roff_positional(usage), help, attrs)
7220    case False{}: []
7221
7222def roff_positionals(items: List<&2, Item>) -> List<&2, String>:
7223  match items:
7224    case Nil{}: Nil{}
7225    case Arg{+usage, help, +attrs} <> tail:
7226      List.append(&2, String,
7227        roff_positional_entry(Bool.and(arg_pos(usage),
7228          Bool.not(has_hidden(attrs))), usage, help, attrs),
7229        roff_positionals(tail))
7230    case head <> tail: roff_positionals(tail)
7231
7232def roff_option_entry(shown: Bool, usage: String, help: String,
7233    attrs: List<&2, Item>) -> List<&2, String>:
7234  match shown:
7235    case True{}: roff_entry(roff_option(usage), help, attrs)
7236    case False{}: []
7237
7238# The visible options under a heading, "" for OPTIONS.
7239def roff_options(items: List<&2, Item>, +heading: String) -> List<&2, String>:
7240  match items:
7241    case Nil{}: Nil{}
7242    case Arg{+usage, help, +attrs} <> tail:
7243      List.append(&2, String,
7244        roff_option_entry(Bool.and(Bool.not(arg_pos(usage)),
7245          Bool.and(Bool.not(has_hidden(attrs)),
7246            String.eq(find_heading(attrs), heading))), usage, help, attrs),
7247        roff_options(tail, heading))
7248    case head <> tail: roff_options(tail, heading)
7249
7250def roff_builtins(opts: List<&2, Opt>) -> List<&2, String>:
7251  match opts:
7252    case Nil{}: Nil{}
7253    case Opt{names, kind, ph, comp, help} <> tail:
7254      ".TP" <> String.join(roff_bolds(names), ", ") <> help <>
7255        roff_builtins(tail)
7256
7257def roff_headings(titles: List<&2, String>, +args: List<&2, Item>) ->
7258    List<&2, String>:
7259  match titles:
7260    case Nil{}: Nil{}
7261    case +title <> tail:
7262      String.concat([".SS \"", roff_text(title), "\""]) <>
7263        List.append(&2, String, roff_options(args, title),
7264          roff_headings(tail, args))
7265
7266def roff_command(hidden: Bool, +page: String, name: String, about: String,
7267    children: List<&2, Item>) -> List<&2, String>:
7268  match hidden:
7269    case True{}: []
7270    case False{}:
7271      [".TP", String.append(roff_bold(String.concat([page, "-", name])), "(1)"),
7272        Roff.esc(join_filled([one_line(about),
7273          tag("aliases", find_aliases(children))]))]
7274
7275def roff_commands(items: List<&2, Item>, +page: String) -> List<&2, String>:
7276  match items:
7277    case Nil{}: Nil{}
7278    case Sub{name, about, +children} <> tail:
7279      List.append(&2, String,
7280        roff_command(has_hidden(children), page, name, about, children),
7281        roff_commands(tail, page))
7282    case head <> tail: roff_commands(tail, page)
7283
7284def roff_arg(pos: Bool, +usage: String, attrs: List<&2, Item>) -> String:
7285  match pos:
7286    case True{}: roff_positional(usage)
7287    case False{}: roff_option(arg_display(arg_info(usage, attrs)))
7288
7289def roff_env_names(names: List<&2, String>) -> List<&2, String>:
7290  match names:
7291    case Nil{}: Nil{}
7292    case name <> tail: roff_bold(name) <> roff_env_names(tail)
7293
7294# How a variable after the first is read, if there is one.
7295def roff_env_order(rest: List<&2, String>) -> String:
7296  match rest:
7297    case Nil{}: ""
7298    case head <> tail: "; the first that is set wins"
7299
7300def roff_env(hidden: Bool, names: List<&2, String>, +usage: String,
7301    attrs: List<&2, Item>) -> List<&2, String>:
7302  match hidden names:
7303    case False{} name <> +tail:
7304      [".TP", String.join(roff_env_names(name <> tail), ", "),
7305        String.concat(["Used for ", roff_arg(arg_pos(usage), usage, attrs),
7306          " when it is not given", roff_env_order(tail)])]
7307    case _ _: []
7308
7309def roff_envs(items: List<&2, Item>) -> List<&2, String>:
7310  match items:
7311    case Nil{}: Nil{}
7312    case Arg{usage, help, +attrs} <> tail:
7313      List.append(&2, String,
7314        roff_env(has_hidden(attrs), find_envs(attrs), usage, attrs),
7315        roff_envs(tail))
7316    case head <> tail: roff_envs(tail)
7317
7318def roff_examples(items: List<&2, Item>, before: List<&2, String>) ->
7319    List<&2, String>:
7320  match items:
7321    case Nil{}: Nil{}
7322    case Example{line, what} <> tail:
7323      List.concat(&2, String, [
7324        before,
7325        [Roff.esc(String.append(what, ":")), ".IP", ".nf",
7326          Roff.esc(String.append("$ ", line)), ".fi"],
7327        roff_examples(tail, [".PP"]),
7328      ])
7329    case head <> tail: roff_examples(tail, before)
7330
7331def sub_pages(items: List<&2, Item>, +page: String) -> List<&2, String>:
7332  match items:
7333    case Nil{}: Nil{}
7334    case Sub{name, about, +children} <> tail:
7335      +rest = sub_pages(tail, page)
7336      Bool.pick(List<&2, String>, has_hidden(children), rest,
7337        String.concat([page, "-", name]) <> rest)
7338    case head <> tail: sub_pages(tail, page)
7339
7340def init_words(words: List<&2, String>) -> List<&2, String>:
7341  match words:
7342    case Nil{}: Nil{}
7343    case word <> Nil{}: Nil{}
7344    case word <> tail: word <> init_words(tail)
7345
7346# The parent's page, none at the root.
7347def parent_pages(words: List<&2, String>) -> List<&2, String>:
7348  match words:
7349    case Nil{}: []
7350    case word <> Nil{}: []
7351    case word <> tail: [String.join(word <> init_words(tail), "-")]
7352
7353def roff_refs(pages: List<&2, String>) -> List<&2, String>:
7354  match pages:
7355    case Nil{}: Nil{}
7356    case page <> tail: String.append(roff_bold(page), "(1)") <> roff_refs(tail)
7357
7358def roff_see(pages: List<&2, String>) -> List<&2, String>:
7359  match pages:
7360    case Nil{}: []
7361    case head <> tail: [String.join(roff_refs(head <> tail), ", ")]
7362
7363def roff_synopsis(texts: List<&2, String>) -> String:
7364  match texts:
7365    case Nil{}: ""
7366    case path <> tail: String.join(roff_bold(path) <> roff_texts(tail), " ")
7367
7368def roff_section(title: String, lines: List<&2, String>) -> List<&2, String>:
7369  match lines:
7370    case Nil{}: []
7371    case head <> tail: String.append(".SH ", title) <> head <> tail
7372
7373def roff_view(view: View, title: String) -> String:
7374  match view:
7375    case View{+path, +about, +items, +globals, root, stuck}:
7376      +words = String.split(path, ' ')
7377      +page = String.join(words, "-")
7378      +args = List.append(&2, Item, items, globals)
7379      text_lines(List.concat(&2, String, [
7380        [String.concat([".TH \"", roff_text(String.to_upper(page)),
7381          "\" \"1\" \"\" \"", roff_text(title), "\" \"User Commands\""]),
7382          ".SH NAME",
7383          Roff.esc(String.concat([page, " - ", one_line(about)])),
7384          ".SH SYNOPSIS",
7385          roff_synopsis(segment_texts(usage_elems(path, items, globals)))],
7386        roff_section("DESCRIPTION", roff_paras(about, ".PP")),
7387        roff_section("ARGUMENTS", roff_positionals(items)),
7388        [".SH OPTIONS"],
7389        roff_options(args, ""),
7390        roff_builtins(builtin_opts(items, globals, root)),
7391        roff_headings(heading_titles(args, []), args),
7392        roff_section("COMMANDS", roff_commands(items, page)),
7393        roff_section("ENVIRONMENT", roff_envs(args)),
7394        [".SH EXIT STATUS", ".TP", "\\fB0\\fR", "Success", ".TP", "\\fB1\\fR",
7395          "Failure", ".TP", "\\fB2\\fR", "Usage error", ".TP", "\\fB70\\fR",
7396          "Internal software error"],
7397        roff_section("EXAMPLES", roff_examples(items, [])),
7398        roff_section("NOTES",
7399          roff_paras(String.join(after_texts(items), "\n\n"), ".PP")),
7400        roff_section("SEE ALSO", roff_see(List.append(&2, String,
7401          parent_pages(words), sub_pages(items, page)))),
7402      ]))
7403
7404def find_version(items: List<&2, Item>) -> String:
7405  match items:
7406    case Nil{}: ""
7407    case Version{value} <> tail: value
7408    case head <> tail: find_version(tail)
7409
7410# The man page of the command at the longest valid prefix of path (§11).
7411def roff(c: Cmd, path: List<&2, String>) -> String:
7412  match c:
7413    case Cmd{+name, about, +items}:
7414      roff_view(view_walk(path, View{name, about, items, [], True{}, False{}}),
7415        join_filled([name, find_version(items)]))
7416
7417# The completion script for shell s (§11).
7418def script(c: Cmd, s: Shell) -> String:
7419  match c s:
7420    case Cmd{name, about, items} Bash{}: bash_script(name, items)
7421    case Cmd{name, about, items} Zsh{}: zsh_script(name, items)
7422    case Cmd{name, about, items} Fish{}: fish_script(name, items)
7423
7424# ---- Outcome (§8.7, V1-V6) ----
7425
7426type Outcome is Data:
7427  Shown{code: U32, out: String, err: String}
7428  Matched{m: Matches}
7429
7430# The subcommand at path, from found on.
7431def sub_at(path: List<&2, String>, items: List<&2, Item>,
7432    found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
7433  match path:
7434    case Nil{}: found
7435    case name <> tail:
7436      +next = find_sub(items, name)
7437      sub_at(tail, sub_items(next), next)
7438
7439def builtin_name(found: Maybe<&2, SubFound>) -> Maybe<&2, String>:
7440  match found:
7441    case Some{SubFound{name, items}}:
7442      Bool.pick(Maybe<&2, String>, has_builtin(items), Some{name}, None{})
7443    case None{}: None{}
7444
7445def field_values(f: Field) -> List<&2, String>:
7446  match f:
7447    case Field{id, values, source}: values
7448
7449# The shell completions was given, which one_of limits to these three.
7450def shell_named(+name: String) -> Shell:
7451  Bool.pick(Shell, String.eq(name, "zsh"), Zsh{},
7452    Bool.pick(Shell, String.eq(name, "fish"), Fish{}, Bash{}))
7453
7454def builtin_page(man: Bool, c: Cmd, fields: List<&2, Field>) -> String:
7455  match man:
7456    case True{}: roff(c, field_values(field_find(fields, "command")))
7457    case False{}:
7458      script(c, shell_named(head_part(field_values(field_find(fields,
7459        "shell")))))
7460
7461# V6: the completions built-in prints its script and man its page, plain;
7462# any other path is left to decode.
7463def outcome_accepted(builtin: Maybe<&2, String>, c: Cmd,
7464    path: List<&2, String>, fields: List<&2, Field>,
7465    external: Maybe<&2, ExternalArgs>) -> Outcome:
7466  match builtin:
7467    case Some{name}: Shown{0, builtin_page(String.eq(name, "man"), c, fields), ""}
7468    case None{}: Matched{Matches{c, path, fields, external}}
7469
7470def outcome_parse(result: ParseResult, +c: Cmd, h: Host,
7471    +argv: List<&2, String>) -> Outcome:
7472  match result:
7473    case Accepted{+path, fields, external}:
7474      outcome_accepted(builtin_name(sub_at(path, cmd_items(c), None{})), c,
7475        path, fields, external)
7476    case HelpRequested{path, long}:
7477      Shown{0, help_out(term(c, h, argv), view_of(c, path), long), ""}
7478    case VersionRequested{}:
7479      Shown{0, String.concat([cmd_name(c), " ", find_version(cmd_items(c)),
7480        "\n"]), ""}
7481    case SummaryRequired{}:
7482      Shown{2, "", summary_err(term(c, h, argv), view_of(c, []))}
7483    case Rejected{path, fault}:
7484      Shown{2, "", fault_err(term(c, h, argv), fault, c, path, argv)}
7485
7486def outcome_lint(found: List<&2, Problem>, +c: Cmd, +h: Host,
7487    +argv: List<&2, String>) -> Outcome:
7488  match found:
7489    case Nil{}: outcome_parse(parse(c, h, argv), c, h, argv)
7490    case p <> tail:
7491      Shown{70, "", String.append("bug: invalid command-line definition\n",
7492        problem_lines(p <> tail))}
7493
7494# eval through V6: a lint failure, help, version, the V3 summary, an error
7495# or a built-in's output is Shown; Matched is a parse left to decode.
7496def outcome(+c: Cmd, h: Host, argv: List<&2, String>) -> Outcome:
7497  outcome_lint(problems(c), c, h, argv)
7498
7499# A Cmd that passed lint, from linted: eval_linted skips the lint eval runs.
7500type Linted is Data:
7501  Linted{cmd: Cmd}
7502
7503def linted_found(found: List<&2, Problem>, c: Cmd) ->
7504    Result<&2, &2, List<&2, Problem>, Linted>:
7505  match found:
7506    case Nil{}: Done{Linted{c}}
7507    case p <> tail: Fail{p <> tail}
7508
7509# Lint c once: a Linted, or lint's problems in the order problems lists.
7510def linted(+c: Cmd) -> Result<&2, &2, List<&2, Problem>, Linted>:
7511  linted_found(problems(c), c)
7512
7513# outcome on a Linted's spec, without the lint.
7514def outcome_linted(l: Linted, +h: Host, +argv: List<&2, String>) -> Outcome:
7515  match l:
7516    case Linted{+c}: outcome_parse(parse(c, h, argv), c, h, argv)
7517
7518# ---- Eval (§6.6, §7.2, §7.3, V7) ----
7519
7520type Step<-A: Type> is Type:
7521  Go{value: A, term: Term}
7522  Quit{code: U32, out: String, err: String}
7523
7524# E12: a decoder's Usage message as an error at the matched scope, printed
7525# as given, in stderr's colors.
7526def usage_err(t: Term, msg: String, view: View) -> String:
7527  match t view:
7528    case Term{out_color, err_color, width}
7529        View{here, about, items, globals, root, stuck}:
7530      emit(err_color, error_page([plain(msg)], [],
7531        usage_block(usage_elems(here, items, globals), page_width(width))))
7532
7533def faulted(-A: Type, f: Fault, +cmd: Cmd, path: List<&2, String>, h: Host,
7534    argv: List<&2, String>) -> Step<A>:
7535  match f:
7536    case Bug{msg}: Quit{70, "", String.concat(["bug: ", msg, "\n"])}
7537    case Usage{msg}:
7538      Quit{2, "", usage_err(term(cmd, h, argv), msg, view_of(cmd, path))}
7539
7540# V7: Done is Go with term's decisions; a fault exits 70 or 2.
7541def decoded(-A: Type, d: Dec(A), cmd: Cmd, path: List<&2, String>, h: Host,
7542    argv: List<&2, String>) -> Step<A>:
7543  match d:
7544    case Done{a}: Go{a, term(cmd, h, argv)}
7545    case Fail{f}: faulted(A, f, cmd, path, h, argv)
7546
7547def eval_step(-A: Type, decode: @+m: Matches -> Dec(A), o: Outcome, h: Host,
7548    argv: List<&2, String>) -> Step<A>:
7549  match o:
7550    case Shown{code, out, err}: Quit{code, out, err}
7551    case Matched{Matches{+cmd, +path, fields, external}}:
7552      decoded(A, decode(Matches{cmd, path, fields, external}), cmd, path, h,
7553        argv)
7554
7555# eval with the decoder as a value, so a law can quantify over decoders.
7556def eval_with(-A: Type, decode: @+m: Matches -> Dec(A), c: Cmd, +h: Host,
7557    +argv: List<&2, String>) -> Step<A>:
7558  eval_step(A, decode, outcome(c, h, argv), h, argv)
7559
7560# The whole CLI as a function. It must stay a direct forwarding definition:
7561# P10 and P11 are proved of eval_with.
7562def eval(~A: Type, ~decode: @+m: Matches -> Dec(A), c: Cmd, h: Host,
7563    argv: List<&2, String>) -> Step<A>:
7564  eval_with(A, decode, c, h, argv)
7565
7566# eval_linted with the decoder as a value.
7567def eval_linted_with(-A: Type, decode: @+m: Matches -> Dec(A), l: Linted,
7568    +h: Host, +argv: List<&2, String>) -> Step<A>:
7569  eval_step(A, decode, outcome_linted(l, h, argv), h, argv)
7570
7571# eval on a Linted, without linting its spec again. It must stay a direct
7572# forwarding definition: P16, p10_linted and p11_linted are proved of
7573# eval_linted_with.
7574def eval_linted(~A: Type, ~decode: @+m: Matches -> Dec(A), l: Linted,
7575    h: Host, argv: List<&2, String>) -> Step<A>:
7576  eval_linted_with(A, decode, l, h, argv)
7577
7578# Each arg's env name in pre-order: an arg's own, then its subcommands'.
7579def env_names(items: List<&2, Item>) -> List<&2, String>:
7580  match items:
7581    case Nil{}: Nil{}
7582    case Arg{usage, help, attrs} <> tail:
7583      List.append(&2, String, find_envs(attrs), env_names(tail))
7584    case Sub{name, about, children} <> tail:
7585      List.append(&2, String, env_names(children), env_names(tail))
7586    case head <> tail: env_names(tail)
7587
7588# kept, then each name not yet in it.
7589def distinct(names: List<&2, String>, kept: List<&2, String>) ->
7590    List<&2, String>:
7591  match names:
7592    case Nil{}: kept
7593    case name <> tail: distinct(tail, append_unique(kept, name))
7594
7595def vars(c: Cmd) -> List<&2, String>:
7596  match c:
7597    case Cmd{name, about, items}:
7598      distinct(List.append(&2, String, env_names(items),
7599        ["NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE", "TERM", "COLUMNS"]), [])
7600
7601# COLUMNS sets the width when as_nat reads it and it is at least 1. digits
7602# is bound because a temporary argument makes String.is_empty take its
7603# string, adding a take to each caller that reuses one (valid_nat).
7604def columns_ok(+value: String) -> Bool:
7605  digits = Nat.read.trim(value)
7606  Bool.and(valid_nat(value), Bool.not(String.is_empty(digits)))
7607
7608def columns_valid(ok: Bool, value: String) -> Maybe<&2, Nat>:
7609  match ok:
7610    case True{}: Some{nat_of(value)}
7611    case False{}: None{}
7612
7613def columns_width(found: Maybe<&2, String>) -> Maybe<&2, Nat>:
7614  match found:
7615    case None{}: None{}
7616    case Some{+value}: columns_valid(columns_ok(value), value)
7617
7618# stdout's columns if it is a TTY, else stderr's if it is one, else 0.
7619def tty_columns(out_tty: Bool, err_tty: Bool, out_cols: U32,
7620    err_cols: U32) -> Nat:
7621  match out_tty err_tty:
7622    case True{} _: U32.to_nat(out_cols)
7623    case False{} True{}: U32.to_nat(err_cols)
7624    case False{} False{}: 0n
7625
7626# first, unless it is 0.
7627def nonzero(first: Nat, then: Nat) -> Nat:
7628  match first:
7629    case 0n: then
7630    case 1n+p: 1n+p
7631
7632def width_or(found: Maybe<&2, Nat>, then: Nat) -> Nat:
7633  match found:
7634    case None{}: then
7635    case Some{width}: width
7636
7637# §7.3: the host from Run.host's readings; the width is COLUMNS, else the
7638# TTY columns, else 80.
7639def probe(+vars: List<&2, Var>, +out_tty: Bool, +err_tty: Bool,
7640    out_cols: U32, err_cols: U32) -> Host:
7641  Host{width_or(columns_width(var_value(vars, "COLUMNS")),
7642    nonzero(tty_columns(out_tty, err_tty, out_cols, err_cols), 80n)),
7643    vars, out_tty, err_tty}
7644
7645def act_code(fits: Bool, n: U32) -> (U32 & String):
7646  match fits:
7647    case True{}: (n, "")
7648    case False{}:
7649      (70, String.concat(["bug: exit code ", U32.show(n),
7650        " is out of range 0..255\n"]))
7651
7652# §7.2: act's code, or 70 with a stderr line when it exceeds 255.
7653def act_exit(+n: U32) -> (U32 & String):
7654  act_code(U32.is_le(n, 255), n)