layout/format.bend

bend-lint-rules@0.2.1.0 · 2,454 lines · 0 definitions

1# layout/format: a file that differs from its formatted text, where it first differs. One formatter
2# owns indentation, spacing, declaration gaps and wrapping.
3# Fix: writes the formatted text.
4# Options: tabWidth (2); wrapAtWidth (100, or "never": no optional wrapping); endOfLine ("lf",
5# "crlf", or "preserve": the file's first, or LF if it has none).
6# Heuristics: it reads tokens, not Bend's syntax, and changes only blanks, line breaks and the space
7# after a comment's #, so a fix keeps the program. A form its token rules do not know can come out
8# laid out oddly.
9#
10# A String is a JS string at run time: ++ is native, and a match on SCon reads one character and slices off the rest.
11# So the lexer reads the text once, tokens carry what the layout asks of them, and docs carry widths.
12#
13# Each loop is go(input, state): a step that does not recurse makes the next state, so every loop recurses on a smaller
14# part of its input. The lexer and the printer recurse on fuel instead: more steps than they can need.
15import Base
16import bend-lint-api@0.2.1.0/linter/lint.bend as Lint
17import bend-lint-api@0.2.1.0/shared/strings.bend as Strings
18
19# Types
20# =====
21type Lex is Data:
22  Code{}
23  Literal{}
24  Comment{}
25  Newline{}
26
27# The spellings the layout tells apart; Word{} is any other, Inc{} ends in n+.
28type Sym is Data:
29  Word{}
30  Comma{}
31  Semi{}
32  Colon{}
33  Ques{}
34  Bang{}
35  Dot{}
36  Tilde{}
37  At{}
38  Bslash{}
39  Plus{}
40  Minus{}
41  Star{}
42  Slash{}
43  Pct{}
44  Caret{}
45  Amp{}
46  Bar{}
47  Lpar{}
48  Rpar{}
49  Lbrk{}
50  Rbrk{}
51  Lbrc{}
52  Rbrc{}
53  Lt{}
54  Gt{}
55  Assign{}
56  Arrow{}
57  Concat{}
58  AndAnd{}
59  OrOr{}
60  Meet{}
61  BitAnd{}
62  BitOr{}
63  BitXor{}
64  Shl{}
65  Shr{}
66  Cons{}
67  Le{}
68  Ge{}
69  EqEq{}
70  Ne{}
71  Inc{}
72  KwReturn{}
73  KwCase{}
74  KwMatch{}
75  KwFor{}
76  KwExs{}
77  KwIs{}
78  KwDo{}
79  KwDef{}
80  KwType{}
81  KwLaw{}
82  KwImport{}
83
84# Offsets and columns count code points. name: whether the text matches
85# /^[\w.]+$/. tail: for a literal across lines, how many characters follow
86# its last newline. from: the text from the token on.
87type Token is Data:
88  Token{
89    text: String,
90    beg: U32,
91    end: U32,
92    col: U32,
93    lex: Lex,
94    sym: Sym,
95    name: Bool,
96    tail: Maybe<&2, U32>,
97    from: String
98  }
99
100# What the lexer read: the end of the text, one whitespace character, or a
101# token; with the text after it and its last character.
102type Scan is Data:
103  Eof{}
104  Blank{rest: String, last: Char, len: U32}
105  Tok{
106    text: String,
107    len: U32,
108    rest: String,
109    last: Char,
110    lex: Lex,
111    sym: Sym,
112    name: Bool,
113    tail: Maybe<&2, U32>
114  }
115
116# Characters read by run: their text and count, the text after them, and
117# the last one.
118type Run is Data:
119  Run{text: String, len: U32, rest: String, last: Char}
120
121# What the next character of a literal is: inside it, escaped, or past it.
122type Quote is Data:
123  Inside{}
124  Escaped{}
125  Shut{}
126
127type Class is Data:
128  IsBlank{}
129  IsHead{}
130  IsDigit{}
131  IsOther{}
132
133type Tree is Data:
134  Leaf{token: Token}
135  Group{open: Token, close: Token, children: List<&2, Tree>, block: Bool}
136
137# A group the tree has opened: the sym that closes it, children last first,
138# and whether a child is a token that makes it a block.
139type Open is Data:
140  Open{token: Token, close: Sym, children: List<&2, Tree>, block: Bool}
141
142type Built is Data:
143  Built{stack: List<&2, Open>, root: List<&2, Tree>}
144
145# A doc printed flat: its width up to the first break it cannot lose (a
146# hard line, or a newline in text), and whether it has one.
147type Flat is Data:
148  Flat{width: U32, brk: Bool}
149
150# Str: text without its trailing whitespace, that whitespace, the width, and
151# how many characters follow the last newline, if any. Line: the text and
152# width when flat. Grp, Nest and Cat keep their Flat; Cat its docs last
153# first.
154type Doc is Data:
155  Str{body: String, blank: String, width: U32, tail: Maybe<&2, U32>}
156  Line{flat: String, width: U32, hard: Bool, offset: U32}
157  Grp{doc: Doc, flat: Flat}
158  Nest{doc: Doc, amount: U32, flat: Flat}
159  Cat{docs: List<&2, Doc>, flat: Flat}
160
161type Measure is Data:
162  Measure{width: U32, tail: Maybe<&2, U32>, keep: U32}
163
164# How build reads a list of trees: a record, a group's children as a raw
165# block, as comma-separated parts, or a group with no children. A block is
166# an embedded statement body: its line and column boundaries mean
167# something, so it keeps them, while ordinary calls inside it still wrap.
168type Mode is Data:
169  Plain{}
170  Block{open: Token, close: Token}
171  Parts{open: Token, close: Token}
172  Bare{open: Token, close: Token}
173
174# A sequence being built: its docs last first, the docs of an operator
175# chain, the previous tree, the indentation levels deepest first, and
176# whether the rest of a run of newlines is skipped.
177type Seq is Data:
178  Seq{
179    out: List<&2, Doc>,
180    chain: Maybe<&2, List<&2, Doc>>,
181    prev: Maybe<&2, Tree>,
182    levels: List<&2, U32>,
183    skip: Bool
184  }
185
186# A comma-separated part: whether it holds only newlines, and whether its
187# last other tree ends in a comment.
188type Part is Data:
189  Part{doc: Doc, newlines: Bool, comment: Bool}
190
191# A group's parts so far, last first; the current part's flags; whether the
192# next tree ends the current part; whether a child starts with a comment.
193type Split is Data:
194  Split{parts: List<&2, Part>, newlines: Bool, comment: Bool, attach: Bool, comments: Bool}
195
196type State is Data:
197  State{seq: Seq, split: Split}
198
199# What a child of a group does to its parts.
200type Act is Data:
201  Add{}
202  AddAttach{}
203  AddEnd{}
204  End{}
205
206type Job is Data:
207  Job{doc: Doc, indent: U32, flat: Bool}
208
209# The printer: jobs left, the text so far as chunks last first, the
210# whitespace not written yet (dropped at a break or at the end), and the
211# column.
212type Print is Data:
213  Print{jobs: List<&2, Job>, out: List<&2, String>, pending: String, col: U32}
214
215type Fit is Data:
216  Go{jobs: List<&2, Job>, left: U32}
217  Stop{fits: Bool}
218
219# Records so far, last first, and the current record, last first.
220type Recs is Data:
221  Recs{done: List<&2, List<&2, Tree>>, current: List<&2, Tree>}
222
223# A declaration record cut after its first colon, if it has one.
224type Cut is Data:
225  Cut{before: List<&2, Tree>, after: List<&2, Tree>, found: Bool}
226
227type Hash is Data:
228  Hash{before: String, comment: Maybe<&2, String>}
229
230# The rows so far, last first, and what the next record needs from them.
231type Rows is Data:
232  Rows{levels: List<&2, U32>, blank: Bool, unsafe: Bool, comment: Bool, rows: List<&2, String>}
233
234# width: None{} for "never".
235type Settings is Data:
236  Settings{tab: U32, width: Maybe<&2, U32>, newline: String}
237
238# Constants
239# =========
240
241# Fuel per code point of a record: more printer steps than any doc needs.
242def fuel_per_point() -> U32:
243  64
244
245# Functions
246# =========
247
248# Text
249# ----
250
251# Whitespace, as Bend's parser reads it.
252def is_blank(+c: Char) -> Bool:
253  match c:
254    case ' ':
255      True{}
256    case '\t':
257      True{}
258    case '\n':
259      True{}
260    case '\r':
261      True{}
262    case _:
263      False{}
264
265def is_head(+c: Char) -> Bool:
266  Char.is_alpha(c) || Char.is_eq(c, '_')
267
268def is_word(+c: Char) -> Bool:
269  is_head(c) || Char.is_digit(c)
270
271def head_passes(~test: @+c: Char -> Bool, s: String) -> Bool:
272  match s:
273    case SNil{}:
274      False{}
275    case SCon{c, rest}:
276      test(c)
277
278def char(c: Char) -> String:
279  SCon{c, SNil{}}
280
281def drop_blank.go(s: String, blank: Bool) -> String:
282  match s blank:
283    case SNil{} _:
284      SNil{}
285    case SCon{c, rest} False{}:
286      SCon{c, rest}
287    case SCon{c, +rest} True{}:
288      drop_blank.go(rest, head_passes(~is_blank, rest))
289
290def drop_blank(+s: String) -> String:
291  drop_blank.go(s, head_passes(~is_blank, s))
292
293def trim_end(s: String) -> String:
294  String.reverse(drop_blank(String.reverse(s)))
295
296def pad(n: Nat, out: String) -> String:
297  match n:
298    case 0n:
299      out
300    case 1n+k:
301      pad(k, SCon{' ', out})
302
303# Base's String.eq is not tail-recursive, so long text would overflow the stack; is_same is.
304# ok: whether the characters so far were the same.
305def is_same.go(a: String, b: String, ok: Bool) -> Bool:
306  match a b ok:
307    case SCon{x, xs} SCon{y, ys} True{}:
308      is_same.go(xs, ys, Char.is_eq(x, y))
309    case SNil{} SNil{} done:
310      done
311    case _ _ _:
312      False{}
313
314def is_same(a: String, b: String) -> Bool:
315  is_same.go(a, b, True{})
316
317# Lexer
318# -----
319#
320# A branch that uses a string or a character its match took apart gets it
321# rebuilt: a string as character + rest, a rope that the next read copies
322# whole, and a character bit by bit. So a def that needs the whole value in
323# a branch takes it twice, as keep, and matches the other copy.
324def is_not_line_end(s: String) -> Bool:
325  match s:
326    case SNil{}:
327      False{}
328    case SCon{'\n', rest}:
329      False{}
330    case SCon{'\r', rest}:
331      False{}
332    case _:
333      True{}
334
335def is_digit_at(s: String) -> Bool:
336  match s:
337    case SNil{}:
338      False{}
339    case SCon{c, rest}:
340      Char.is_digit(c)
341
342def is_ident_at.of(dot: Bool, c: Char, rest: String) -> Bool:
343  match dot:
344    case True{}:
345      head_passes(~is_head, rest)
346    case False{}:
347      is_word(c)
348
349# A word character, or a dot before the start of a word.
350def is_ident_at(s: String) -> Bool:
351  match s:
352    case SNil{}:
353      False{}
354    case SCon{+c, rest}:
355      is_ident_at.of(Char.is_eq(c, '.'), c, rest)
356
357# `scan`, with the text so far last character first: on the C lane, ++ copies its left side.
358def scan.go(
359  ~test: String -> Bool,
360  s: String,
361  keep: String,
362  ok: Bool,
363  text: String,
364  n: U32,
365  last: Char
366) -> Run:
367  match s ok:
368    case SCon{+c, +rest} True{}:
369      scan.go(~test, rest, rest, test(rest), SCon{c, text}, (n + 1: U32), c)
370    case _ _:
371      Run{String.reverse(text), n, keep, last}
372
373# Reads characters while test holds, after text; test sees the text from
374# each character on, so it can look ahead.
375def scan(
376  ~test: String -> Bool,
377  s: String,
378  keep: String,
379  ok: Bool,
380  text: String,
381  n: U32,
382  last: Char
383) -> Run:
384  scan.go(~test, s, keep, ok, String.reverse(text), n, last)
385
386def tok(r: Run, sym: Sym, name: Bool) -> Scan:
387  match r:
388    case Run{text, len, rest, last}:
389      Tok{text, len, rest, last, Code{}, sym, name, None{}}
390
391def fixed(text: String, len: U32, rest: String, last: Char, sym: Sym) -> Scan:
392  Tok{text, len, rest, last, Code{}, sym, False{}, None{}}
393
394def keyword.is(text: String, word: String, sym: Sym) -> Sym:
395  Bool.pick(Sym, is_same(text, word), sym, Word{})
396
397def keyword(text: String, +keep: String) -> Sym:
398  match text:
399    case SCon{'r', rest}:
400      keyword.is(keep, "return", KwReturn{})
401    case SCon{'c', rest}:
402      keyword.is(keep, "case", KwCase{})
403    case SCon{'m', rest}:
404      keyword.is(keep, "match", KwMatch{})
405    case SCon{'f', rest}:
406      keyword.is(keep, "for", KwFor{})
407    case SCon{'e', rest}:
408      keyword.is(keep, "exs", KwExs{})
409    case SCon{'i', rest}:
410      Bool.pick(Sym, is_same(keep, "is"), KwIs{}, keyword.is(keep, "import", KwImport{}))
411    case SCon{'d', rest}:
412      Bool.pick(Sym, is_same(keep, "do"), KwDo{}, keyword.is(keep, "def", KwDef{}))
413    case SCon{'t', rest}:
414      keyword.is(keep, "type", KwType{})
415    case SCon{'l', rest}:
416      keyword.is(keep, "law", KwLaw{})
417    case _:
418      Word{}
419
420def ident(r: Run) -> Scan:
421  match r:
422    case Run{+text, len, rest, last}:
423      Tok{text, len, rest, last, Code{}, keyword(text, text), True{}, None{}}
424
425def number.tok(r: Run, inc: Bool, name: Bool) -> Scan:
426  tok(r, Bool.pick(Sym, inc, Inc{}, Word{}), name)
427
428def exponent.digits(
429  go: Bool,
430  +rest: String,
431  mark: String,
432  k: U32,
433  keep: String,
434  text: String,
435  n: U32,
436  last: Char,
437  signed: Bool
438) -> Scan:
439  match go:
440    case True{}:
441      number.tok(
442        scan(~is_digit_at, rest, rest, True{}, text ++ mark, (n + k: U32), last),
443        False{},
444        Bool.not(signed)
445      )
446    case False{}:
447      number.tok(Run{text, n, keep, last}, False{}, True{})
448
449def exponent.sign(
450  rest: String,
451  +again: String,
452  mark: String,
453  keep: String,
454  text: String,
455  n: U32,
456  last: Char
457) -> Scan:
458  match rest:
459    case SCon{'+', +r}:
460      exponent.digits(is_digit_at(r), r, mark ++ "+", 2, keep, text, n, last, True{})
461    case SCon{'-', +r}:
462      exponent.digits(is_digit_at(r), r, mark ++ "-", 2, keep, text, n, last, True{})
463    case _:
464      exponent.digits(is_digit_at(again), again, mark, 1, keep, text, n, last, False{})
465
466def exponent.of(s: String, keep: String, text: String, n: U32, last: Char) -> Scan:
467  match s:
468    case SCon{'e', +rest}:
469      exponent.sign(rest, rest, "e", keep, text, n, last)
470    case SCon{'E', +rest}:
471      exponent.sign(rest, rest, "E", keep, text, n, last)
472    case _:
473      number.tok(Run{text, n, keep, last}, False{}, True{})
474
475# After a fraction: [eE][+-]?\d+, if it is there.
476def exponent(r: Run) -> Scan:
477  match r:
478    case Run{text, n, +s, last}:
479      exponent.of(s, s, text, n, last)
480
481def fraction(go: Bool, +rest: String, keep: String, text: String, n: U32, last: Char) -> Scan:
482  match go:
483    case True{}:
484      exponent(scan(~is_digit_at, rest, rest, True{}, text ++ ".", (n + 1: U32), '.'))
485    case False{}:
486      number.tok(Run{text, n, keep, last}, False{}, True{})
487
488# After "n": n+, or n alone.
489def number.inc(s: String, keep: String, text: String, n: U32) -> Scan:
490  match s:
491    case SCon{'+', rest}:
492      number.tok(Run{text ++ "n+", (n + 2: U32), rest, '+'}, True{}, False{})
493    case _:
494      number.tok(Run{text ++ "n", (n + 1: U32), keep, 'n'}, False{}, True{})
495
496def number.of(s: String, keep: String, text: String, n: U32, last: Char) -> Scan:
497  match s:
498    case SCon{'n', +rest}:
499      number.inc(rest, rest, text, n)
500    case SCon{'.', +rest}:
501      fraction(is_digit_at(rest), rest, keep, text, n, last)
502    case _:
503      number.tok(Run{text, n, keep, last}, False{}, True{})
504
505# After a number's digits: n, n+, or a fraction and its exponent.
506def number(r: Run) -> Scan:
507  match r:
508    case Run{text, n, +s, last}:
509      number.of(s, s, text, n, last)
510
511def tail_after(c: Char, tail: Maybe<&2, U32>) -> Maybe<&2, U32>:
512  match c tail:
513    case '\n' _:
514      Some{0}
515    case _ Some{t}:
516      Some{(t + 1: U32)}
517    case _ None{}:
518      None{}
519
520def quote_after(+c: Char, quote: Char) -> Quote:
521  Bool.pick(
522    Quote,
523    Char.is_eq(c, '\\'),
524    Escaped{},
525    Bool.pick(Quote, Char.is_eq(c, quote), Shut{}, Inside{})
526  )
527
528# `literal`, with the text so far last character first: on the C lane, ++ copies its left side.
529def literal.go(
530  s: String,
531  keep: String,
532  +quote: Char,
533  at: Quote,
534  text: String,
535  n: U32,
536  last: Char,
537  tail: Maybe<&2, U32>
538) -> Scan:
539  match s at:
540    case SCon{+c, +rest} Inside{}:
541      literal.go(
542        rest,
543        rest,
544        quote,
545        quote_after(c, quote),
546        SCon{c, text},
547        (n + 1: U32),
548        c,
549        tail_after(c, tail)
550      )
551    case SCon{+c, +rest} Escaped{}:
552      literal.go(rest, rest, quote, Inside{}, SCon{c, text}, (n + 1: U32), c, tail_after(c, tail))
553    case _ _:
554      Tok{String.reverse(text), n, keep, last, Literal{}, Word{}, False{}, tail}
555
556def literal(
557  s: String,
558  keep: String,
559  quote: Char,
560  at: Quote,
561  text: String,
562  n: U32,
563  last: Char,
564  tail: Maybe<&2, U32>
565) -> Scan:
566  literal.go(s, keep, quote, at, String.reverse(text), n, last, tail)
567
568def class_of(+c: Char) -> Class:
569  Bool.pick(
570    Class,
571    is_blank(c),
572    IsBlank{},
573    Bool.pick(Class, is_head(c), IsHead{}, Bool.pick(Class, Char.is_digit(c), IsDigit{}, IsOther{}))
574  )
575
576def single(c: Char) -> Sym:
577  match c:
578    case ',':
579      Comma{}
580    case ';':
581      Semi{}
582    case ':':
583      Colon{}
584    case '?':
585      Ques{}
586    case '~':
587      Tilde{}
588    case '@':
589      At{}
590    case '\\':
591      Bslash{}
592    case '*':
593      Star{}
594    case '/':
595      Slash{}
596    case '%':
597      Pct{}
598    case '^':
599      Caret{}
600    case '(':
601      Lpar{}
602    case ')':
603      Rpar{}
604    case '[':
605      Lbrk{}
606    case ']':
607      Rbrk{}
608    case '{':
609      Lbrc{}
610    case '}':
611      Rbrc{}
612    case _:
613      Word{}
614
615def lex.other(class: Class, +c: Char, +rest: String) -> Scan:
616  match class:
617    case IsBlank{}:
618      Blank{rest, c, 1}
619    case IsHead{}:
620      ident(scan(~is_ident_at, rest, rest, is_ident_at(rest), char(c), 1, c))
621    case IsDigit{}:
622      number(scan(~is_digit_at, rest, rest, is_digit_at(rest), char(c), 1, c))
623    case IsOther{}:
624      fixed(char(c), 1, rest, c, single(c))
625
626def lex.char(s: String) -> Scan:
627  match s:
628    case SNil{}:
629      Eof{}
630    case SCon{+c, rest}:
631      lex.other(class_of(c), c, rest)
632
633def lex.dash(word: Bool, keep: String, r: String) -> Scan:
634  match word:
635    case True{}:
636      fixed("<", 1, keep, '<', Lt{})
637    case False{}:
638      fixed("<-", 2, r, '-', Word{})
639
640def lex.lt(rest: String, keep: String, +prev: Char) -> Scan:
641  match rest:
642    case SCon{'&', SCon{'>', r}}:
643      fixed("<&>", 3, r, '>', Meet{})
644    case SCon{'-', r}:
645      lex.dash(is_word(prev) || Char.is_eq(prev, '.'), keep, r)
646    case SCon{'=', r}:
647      fixed("<=", 2, r, '=', Le{})
648    case SCon{'<', r}:
649      fixed("<<", 2, r, '<', Shl{})
650    case SCon{'>', r}:
651      fixed("<>", 2, r, '>', Cons{})
652    case _:
653      fixed("<", 1, keep, '<', Lt{})
654
655def lex.shr(blank: Bool, keep: String, r: String) -> Scan:
656  match blank:
657    case True{}:
658      fixed(">>", 2, r, '>', Shr{})
659    case False{}:
660      fixed(">", 1, keep, '>', Gt{})
661
662def lex.gt(rest: String, keep: String, prev: Char) -> Scan:
663  match rest:
664    case SCon{'>', r}:
665      lex.shr(is_blank(prev), keep, r)
666    case SCon{'=', r}:
667      fixed(">=", 2, r, '=', Ge{})
668    case _:
669      fixed(">", 1, keep, '>', Gt{})
670
671def lex.dot(rest: String, keep: String) -> Scan:
672  match rest:
673    case SCon{'&', SCon{'.', r}}:
674      fixed(".&.", 3, r, '.', BitAnd{})
675    case SCon{'|', SCon{'.', r}}:
676      fixed(".|.", 3, r, '.', BitOr{})
677    case SCon{'^', SCon{'.', r}}:
678      fixed(".^.", 3, r, '.', BitXor{})
679    case _:
680      Tok{".", 1, keep, '.', Code{}, Dot{}, True{}, None{}}
681
682def lex.minus(rest: String, keep: String) -> Scan:
683  match rest:
684    case SCon{'>', r}:
685      fixed("->", 2, r, '>', Arrow{})
686    case _:
687      fixed("-", 1, keep, '-', Minus{})
688
689def lex.eq(rest: String, keep: String) -> Scan:
690  match rest:
691    case SCon{'=', r}:
692      fixed("==", 2, r, '=', EqEq{})
693    case SCon{'>', r}:
694      fixed("=>", 2, r, '>', Word{})
695    case _:
696      fixed("=", 1, keep, '=', Assign{})
697
698def lex.bang(rest: String, keep: String) -> Scan:
699  match rest:
700    case SCon{'=', r}:
701      fixed("!=", 2, r, '=', Ne{})
702    case _:
703      fixed("!", 1, keep, '!', Bang{})
704
705def lex.amp(rest: String, keep: String) -> Scan:
706  match rest:
707    case SCon{'&', r}:
708      fixed("&&", 2, r, '&', AndAnd{})
709    case SCon{'0', r}:
710      fixed("&0", 2, r, '0', Word{})
711    case SCon{'1', r}:
712      fixed("&1", 2, r, '1', Word{})
713    case SCon{'2', r}:
714      fixed("&2", 2, r, '2', Word{})
715    case _:
716      fixed("&", 1, keep, '&', Amp{})
717
718def lex.bar(rest: String, keep: String) -> Scan:
719  match rest:
720    case SCon{'|', r}:
721      fixed("||", 2, r, '|', OrOr{})
722    case _:
723      fixed("|", 1, keep, '|', Bar{})
724
725def lex.plus(rest: String, keep: String) -> Scan:
726  match rest:
727    case SCon{'+', r}:
728      fixed("++", 2, r, '+', Concat{})
729    case _:
730      fixed("+", 1, keep, '+', Plus{})
731
732def comment(r: Run) -> Scan:
733  match r:
734    case Run{text, len, rest, last}:
735      Tok{text, len, rest, last, Comment{}, Word{}, False{}, None{}}
736
737# A run of whitespace on one line.
738def blanks(s: String, keep: String, n: U32, last: Char) -> Scan:
739  match s:
740    case SCon{' ', +rest}:
741      blanks(rest, rest, (n + 1: U32), ' ')
742    case SCon{'\t', +rest}:
743      blanks(rest, rest, (n + 1: U32), '\t')
744    case SCon{'\r', +rest}:
745      blanks(rest, rest, (n + 1: U32), '\r')
746    case _:
747      Blank{keep, last, n}
748
749# The token at the start of s; prev is the character before it. Literals
750# and comment text are kept verbatim: a quote or # inside a string is not
751# syntax. Successor notation (1n+) and quantities (&2) are single tokens.
752def lex.token(s: String, keep: String, prev: Char) -> Scan:
753  match s:
754    case SNil{}:
755      Eof{}
756    case SCon{' ', +rest}:
757      blanks(rest, rest, 1, ' ')
758    case SCon{'\n', rest}:
759      Tok{"\n", 1, rest, '\n', Newline{}, Word{}, False{}, None{}}
760    case SCon{'#', +rest}:
761      comment(scan(~is_not_line_end, rest, rest, is_not_line_end(rest), "#", 1, '#'))
762    case SCon{'"', +rest}:
763      literal(rest, rest, '"', Inside{}, "\"", 1, '"', None{})
764    case SCon{'\'', +rest}:
765      literal(rest, rest, '\'', Inside{}, "'", 1, '\'', None{})
766    case SCon{'<', +rest}:
767      lex.lt(rest, rest, prev)
768    case SCon{'>', +rest}:
769      lex.gt(rest, rest, prev)
770    case SCon{'.', +rest}:
771      lex.dot(rest, rest)
772    case SCon{'-', +rest}:
773      lex.minus(rest, rest)
774    case SCon{'=', +rest}:
775      lex.eq(rest, rest)
776    case SCon{'!', +rest}:
777      lex.bang(rest, rest)
778    case SCon{'&', +rest}:
779      lex.amp(rest, rest)
780    case SCon{'|', +rest}:
781      lex.bar(rest, rest)
782    case SCon{'+', +rest}:
783      lex.plus(rest, rest)
784    case _:
785      lex.char(keep)
786
787def next_col(lex: Lex, tail: Maybe<&2, U32>, col: U32, len: U32) -> U32:
788  match lex tail:
789    case Newline{} _:
790      0
791    case _ Some{t}:
792      t
793    case _ None{}:
794      (col + len: U32)
795
796# Each step reads at least one character, so the text's length is fuel.
797def lex.go(
798  fuel: Nat,
799  sc: Scan,
800  from: String,
801  +at: U32,
802  +col: U32,
803  acc: List<&2, Token>
804) -> List<&2, Token>:
805  match fuel sc:
806    case 1n+k Tok{text, +len, +rest, last, +lex, sym, name, +tail}:
807      lex.go(
808        k,
809        lex.token(rest, rest, last),
810        rest,
811        (at + len: U32),
812        next_col(lex, tail, col, len),
813        Con{Token{text, at, (at + len: U32), col, lex, sym, name, tail, from}, acc}
814      )
815    case 1n+k Blank{+rest, last, +len}:
816      lex.go(k, lex.token(rest, rest, last), rest, (at + len: U32), (col + len: U32), acc)
817    case _ _:
818      List.reverse(&2, Token, acc)
819
820def tokens(+s: String) -> List<&2, Token>:
821  lex.go(1n+String.length(s), lex.token(s, s, '\0'), s, 0, 0, [])
822
823# Tokens
824# ------
825def Token.text(t: Token) -> String:
826  match t:
827    case Token{text, beg, end, col, lex, sym, name, tail, from}:
828      text
829
830def Token.beg(t: Token) -> U32:
831  match t:
832    case Token{text, beg, end, col, lex, sym, name, tail, from}:
833      beg
834
835def Token.end(t: Token) -> U32:
836  match t:
837    case Token{text, beg, end, col, lex, sym, name, tail, from}:
838      end
839
840def Token.col(t: Token) -> U32:
841  match t:
842    case Token{text, beg, end, col, lex, sym, name, tail, from}:
843      col
844
845def Token.sym(t: Token) -> Sym:
846  match t:
847    case Token{text, beg, end, col, lex, sym, name, tail, from}:
848      sym
849
850def Token.is_name(t: Token) -> Bool:
851  match t:
852    case Token{text, beg, end, col, lex, sym, name, tail, from}:
853      name
854
855def Token.from(t: Token) -> String:
856  match t:
857    case Token{text, beg, end, col, lex, sym, name, tail, from}:
858      from
859
860def is_comment(t: Token) -> Bool:
861  match t:
862    case Token{text, beg, end, col, Comment{}, sym, name, tail, from}:
863      True{}
864    case _:
865      False{}
866
867def is_operator(s: Sym) -> Bool:
868  match s:
869    case Plus{}:
870      True{}
871    case Minus{}:
872      True{}
873    case Star{}:
874      True{}
875    case Slash{}:
876      True{}
877    case Pct{}:
878      True{}
879    case Caret{}:
880      True{}
881    case Concat{}:
882      True{}
883    case AndAnd{}:
884      True{}
885    case OrOr{}:
886      True{}
887    case Meet{}:
888      True{}
889    case BitAnd{}:
890      True{}
891    case BitOr{}:
892      True{}
893    case BitXor{}:
894      True{}
895    case Shl{}:
896      True{}
897    case Shr{}:
898      True{}
899    case Cons{}:
900      True{}
901    case Lt{}:
902      True{}
903    case Gt{}:
904      True{}
905    case Le{}:
906      True{}
907    case Ge{}:
908      True{}
909    case EqEq{}:
910      True{}
911    case Ne{}:
912      True{}
913    case Amp{}:
914      True{}
915    case Bar{}:
916      True{}
917    case _:
918      False{}
919
920# A sign or quantity glued to what follows it.
921def is_prefix(s: Sym) -> Bool:
922  match s:
923    case Plus{}:
924      True{}
925    case Minus{}:
926      True{}
927    case Pct{}:
928      True{}
929    case Amp{}:
930      True{}
931    case _:
932      False{}
933
934def is_tight(s: Sym) -> Bool:
935  match s:
936    case Comma{}:
937      True{}
938    case Semi{}:
939      True{}
940    case Colon{}:
941      True{}
942    case Ques{}:
943      True{}
944    case Bang{}:
945      True{}
946    case Dot{}:
947      True{}
948    case _:
949      False{}
950
951def is_punct(s: Sym) -> Bool:
952  match s:
953    case Comma{}:
954      True{}
955    case Semi{}:
956      True{}
957    case Colon{}:
958      True{}
959    case _:
960      False{}
961
962def is_sigil(s: Sym) -> Bool:
963  match s:
964    case Tilde{}:
965      True{}
966    case At{}:
967      True{}
968    case Ques{}:
969      True{}
970    case Bang{}:
971      True{}
972    case Bslash{}:
973      True{}
974    case _:
975      False{}
976
977def is_opener(s: Sym) -> Bool:
978  match s:
979    case Lpar{}:
980      True{}
981    case Lbrk{}:
982      True{}
983    case Lbrc{}:
984      True{}
985    case Lt{}:
986      True{}
987    case _:
988      False{}
989
990def is_closer(s: Sym) -> Bool:
991  match s:
992    case Rpar{}:
993      True{}
994    case Rbrk{}:
995      True{}
996    case Rbrc{}:
997      True{}
998    case _:
999      False{}
1000
1001def is_keyword(s: Sym) -> Bool:
1002  match s:
1003    case KwReturn{}:
1004      True{}
1005    case KwCase{}:
1006      True{}
1007    case KwMatch{}:
1008      True{}
1009    case KwFor{}:
1010      True{}
1011    case KwExs{}:
1012      True{}
1013    case KwIs{}:
1014      True{}
1015    case _:
1016      False{}
1017
1018def is_block_word(s: Sym) -> Bool:
1019  match s:
1020    case KwMatch{}:
1021      True{}
1022    case KwCase{}:
1023      True{}
1024    case KwDo{}:
1025      True{}
1026    case Assign{}:
1027      True{}
1028    case Semi{}:
1029      True{}
1030    case Pct{}:
1031      True{}
1032    case Bslash{}:
1033      True{}
1034    case _:
1035      False{}
1036
1037# A record that ends in one of these goes on at the next line.
1038def is_continued(s: Sym) -> Bool:
1039  match s:
1040    case Arrow{}:
1041      True{}
1042    case Assign{}:
1043      True{}
1044    case Concat{}:
1045      True{}
1046    case AndAnd{}:
1047      True{}
1048    case OrOr{}:
1049      True{}
1050    case _:
1051      False{}
1052
1053def is_declaration(s: Sym) -> Bool:
1054  match s:
1055    case KwDef{}:
1056      True{}
1057    case KwType{}:
1058      True{}
1059    case KwLaw{}:
1060      True{}
1061    case At{}:
1062      True{}
1063    case _:
1064      False{}
1065
1066def is_import(s: Sym) -> Bool:
1067  match s:
1068    case KwImport{}:
1069      True{}
1070    case _:
1071      False{}
1072
1073def is_sym(s: Sym, t: Sym) -> Bool:
1074  match s t:
1075    case Comma{} Comma{}:
1076      True{}
1077    case Colon{} Colon{}:
1078      True{}
1079    case Lbrc{} Lbrc{}:
1080      True{}
1081    case Gt{} Gt{}:
1082      True{}
1083    case Rpar{} Rpar{}:
1084      True{}
1085    case Rbrk{} Rbrk{}:
1086      True{}
1087    case Rbrc{} Rbrc{}:
1088      True{}
1089    case Bang{} Bang{}:
1090      True{}
1091    case At{} At{}:
1092      True{}
1093    case _ _:
1094      False{}
1095
1096# Trees
1097# -----
1098def first(n: Tree) -> Token:
1099  match n:
1100    case Leaf{t}:
1101      t
1102    case Group{open, close, children, block}:
1103      open
1104
1105def last(n: Tree) -> Token:
1106  match n:
1107    case Leaf{t}:
1108      t
1109    case Group{open, close, children, block}:
1110      close
1111
1112def is_newline(n: Tree) -> Bool:
1113  match n:
1114    case Leaf{Token{text, beg, end, col, Newline{}, sym, name, tail, from}}:
1115      True{}
1116    case _:
1117      False{}
1118
1119def is_leaf(n: Tree) -> Bool:
1120  match n:
1121    case Leaf{t}:
1122      True{}
1123    case _:
1124      False{}
1125
1126def is_leaf_of(n: Tree, s: Sym) -> Bool:
1127  match n:
1128    case Leaf{t}:
1129      is_sym(Token.sym(t), s)
1130    case _:
1131      False{}
1132
1133def head(xs: List<&2, Tree>) -> Maybe<&2, Tree>:
1134  match xs:
1135    case Nil{}:
1136      None{}
1137    case Con{x, rest}:
1138      Some{x}
1139
1140def last_of(xs: List<&2, Tree>, at: Maybe<&2, Tree>) -> Maybe<&2, Tree>:
1141  match xs:
1142    case Nil{}:
1143      at
1144    case Con{x, rest}:
1145      last_of(rest, Some{x})
1146
1147# The first tree after a run of newlines.
1148def upcoming(xs: List<&2, Tree>) -> Maybe<&2, Tree>:
1149  match xs:
1150    case Nil{}:
1151      None{}
1152    case Con{Leaf{Token{text, beg, end, col, Newline{}, sym, name, tail, from}}, rest}:
1153      upcoming(rest)
1154    case Con{x, rest}:
1155      Some{x}
1156
1157# What closes the group s opens, or Word{}.
1158def closer(s: Sym) -> Sym:
1159  match s:
1160    case Lpar{}:
1161      Rpar{}
1162    case Lbrk{}:
1163      Rbrk{}
1164    case Lbrc{}:
1165      Rbrc{}
1166    case Lt{}:
1167      Gt{}
1168    case _:
1169      Word{}
1170
1171def is_angled(prev: Maybe<&2, Token>, t: Token) -> Bool:
1172  match prev:
1173    case Some{+p}:
1174      U32.is_eq(Token.end(p), Token.beg(t)) && Token.is_name(p)
1175    case None{}:
1176      False{}
1177
1178# Whether t opens a group: a glued < opens type arguments.
1179def opens.of(s: Sym, t: Token, prev: Maybe<&2, Token>) -> Bool:
1180  match s:
1181    case Lpar{}:
1182      True{}
1183    case Lbrk{}:
1184      True{}
1185    case Lbrc{}:
1186      True{}
1187    case Lt{}:
1188      is_angled(prev, t)
1189    case _:
1190      False{}
1191
1192def opens(+t: Token, prev: Maybe<&2, Token>) -> Bool:
1193  opens.of(Token.sym(t), t, prev)
1194
1195def is_block_leaf(n: Tree) -> Bool:
1196  match n:
1197    case Leaf{t}:
1198      is_block_word(Token.sym(t))
1199    case _:
1200      False{}
1201
1202def add(+n: Tree, stack: List<&2, Open>, root: List<&2, Tree>) -> Built:
1203  match stack:
1204    case Nil{}:
1205      Built{Nil{}, Con{n, root}}
1206    case Con{Open{t, close, children, block}, rest}:
1207      Built{Con{Open{t, close, Con{n, children}, block || is_block_leaf(n)}, rest}, root}
1208
1209def waits(stack: List<&2, Open>, s: Sym) -> Bool:
1210  match stack:
1211    case Nil{}:
1212      False{}
1213    case Con{Open{t, close, children, block}, rest}:
1214      is_sym(close, s)
1215
1216def pop(t: Token, stack: List<&2, Open>, root: List<&2, Tree>) -> Built:
1217  match stack:
1218    case Nil{}:
1219      Built{Nil{}, root}
1220    case Con{Open{open, close, children, block}, rest}:
1221      add(Group{open, t, List.reverse(&2, Tree, children), block}, rest, root)
1222
1223def tree.close(closes: Bool, t: Token, stack: List<&2, Open>, root: List<&2, Tree>) -> Built:
1224  match closes:
1225    case True{}:
1226      pop(t, stack, root)
1227    case False{}:
1228      add(Leaf{t}, stack, root)
1229
1230def tree.step(opens: Bool, +t: Token, +stack: List<&2, Open>, root: List<&2, Tree>) -> Built:
1231  match opens:
1232    case True{}:
1233      Built{Con{Open{t, closer(Token.sym(t)), [], False{}}, stack}, root}
1234    case False{}:
1235      tree.close(waits(stack, Token.sym(t)), t, stack, root)
1236
1237def tree.go(ts: List<&2, Token>, prev: Maybe<&2, Token>, b: Built) -> Maybe<&2, List<&2, Tree>>:
1238  match ts b:
1239    case Nil{} Built{Nil{}, root}:
1240      Some{List.reverse(&2, Tree, root)}
1241    case Nil{} Built{Con{o, os}, root}:
1242      None{}
1243    case Con{+t, rest} Built{stack, root}:
1244      tree.go(rest, Some{t}, tree.step(opens(t, prev), t, stack, root))
1245
1246# Docs
1247# ----
1248def measure(s: String, +n: U32, tail: Maybe<&2, U32>, keep: U32) -> Measure:
1249  match s:
1250    case SNil{}:
1251      Measure{n, tail, keep}
1252    case SCon{+c, rest}:
1253      measure(
1254        rest,
1255        (n + 1: U32),
1256        tail_after(c, tail),
1257        Bool.pick(U32, is_blank(c), keep, (n + 1: U32))
1258      )
1259
1260def str.split(whole: Bool, +s: String, width: U32, tail: Maybe<&2, U32>, +keep: U32) -> Doc:
1261  match whole:
1262    case True{}:
1263      Str{s, "", width, tail}
1264    case False{}:
1265      Str{Strings.take(s, U32.to_nat(keep)), String.drop(s, U32.to_nat(keep)), width, tail}
1266
1267def str.of(s: String, m: Measure) -> Doc:
1268  match m:
1269    case Measure{+width, tail, +keep}:
1270      str.split(U32.is_eq(width, keep), s, width, tail, keep)
1271
1272def str(+s: String) -> Doc:
1273  str.of(s, measure(s, 0, None{}, 0))
1274
1275# A doc for text of this width, with no newline and no trailing whitespace.
1276def text(s: String, width: U32) -> Doc:
1277  Str{s, "", width, None{}}
1278
1279def space(s: String, width: U32) -> Doc:
1280  Str{"", s, width, None{}}
1281
1282def nothing() -> Doc:
1283  Str{"", "", 0, None{}}
1284
1285def soft(flat: String, width: U32) -> Doc:
1286  Line{flat, width, False{}, 0}
1287
1288def hard(offset: U32) -> Doc:
1289  Line{"", 0, True{}, offset}
1290
1291def flat_of(d: Doc) -> Flat:
1292  match d:
1293    case Str{body, blank, width, Some{t}}:
1294      Flat{0, True{}}
1295    case Str{body, blank, width, None{}}:
1296      Flat{width, False{}}
1297    case Line{text, width, True{}, offset}:
1298      Flat{0, True{}}
1299    case Line{text, width, False{}, offset}:
1300      Flat{width, False{}}
1301    case Grp{doc, f}:
1302      f
1303    case Nest{doc, amount, f}:
1304      f
1305    case Cat{docs, f}:
1306      f
1307
1308# The Flat of a doc a, then of what follows it.
1309def flat_then(a: Flat, rest: Flat) -> Flat:
1310  match a rest:
1311    case Flat{w, True{}} r:
1312      Flat{w, True{}}
1313    case Flat{w, False{}} Flat{v, b}:
1314      Flat{(w + v: U32), b}
1315
1316def flat_rev(docs: List<&2, Doc>, acc: Flat) -> Flat:
1317  match docs:
1318    case Nil{}:
1319      acc
1320    case Con{d, rest}:
1321      flat_rev(rest, flat_then(flat_of(d), acc))
1322
1323def grp(+d: Doc) -> Doc:
1324  Grp{d, flat_of(d)}
1325
1326def nest(+d: Doc, amount: U32) -> Doc:
1327  Nest{d, amount, flat_of(d)}
1328
1329# A Cat of docs last first.
1330def cat_rev(+docs: List<&2, Doc>) -> Doc:
1331  Cat{docs, flat_rev(docs, Flat{0, False{}})}
1332
1333# A Cat of a few docs, in order.
1334def cat(docs: List<&2, Doc>) -> Doc:
1335  cat_rev(List.reverse(&2, Doc, docs))
1336
1337def code(t: Token) -> Doc:
1338  match t:
1339    case Token{text, beg, end, col, lex, sym, name, tail, from}:
1340      Str{text, "", (end - beg: U32), None{}}
1341
1342def is_glued_prefix(+a: Token, b: Maybe<&2, Tree>) -> Bool:
1343  match b:
1344    case Some{n}:
1345      is_prefix(Token.sym(a)) && U32.is_eq(Token.end(a), Token.beg(first(n)))
1346    case None{}:
1347      False{}
1348
1349# Whether a group after x is its argument list or type arguments.
1350def is_callee(+x: Token) -> Bool:
1351  (
1352    Token.is_name(x) || is_sym(Token.sym(x), Bang{}) || is_closer(Token.sym(x))
1353  )
1354    && Bool.not(is_keyword(Token.sym(x)))
1355
1356def is_inc(s: Sym) -> Bool:
1357  match s:
1358    case Inc{}:
1359      True{}
1360    case _:
1361      False{}
1362
1363# What goes between x and the tree b that y starts. A constructor's brace
1364# is glued to its name; a brace after a space can instead be the next
1365# (annotated) argument of a call without commas, so the space stays. Other
1366# atoms need a space.
1367def separator.of(+x: Token, +y: Token, +group: Bool, b: Tree) -> Doc:
1368  Bool.pick(
1369    Doc,
1370    is_comment(y),
1371    space("  ", 2),
1372    Bool.pick(
1373      Doc,
1374      is_tight(Token.sym(y)),
1375      nothing(),
1376      Bool.pick(
1377        Doc,
1378        is_punct(Token.sym(x)),
1379        space(" ", 1),
1380        Bool.pick(
1381          Doc,
1382          is_sigil(Token.sym(x)),
1383          nothing(),
1384          Bool.pick(
1385            Doc,
1386            is_glued_prefix(x, Some{b}),
1387            nothing(),
1388            Bool.pick(
1389              Doc,
1390              is_inc(Token.sym(x)),
1391              nothing(),
1392              Bool.pick(
1393                Doc,
1394                group && is_sym(Token.sym(y), Lbrc{}) && U32.is_ne(Token.end(x), Token.beg(y)),
1395                space(" ", 1),
1396                Bool.pick(
1397                  Doc,
1398                  group && is_opener(Token.sym(y)) && is_callee(x),
1399                  nothing(),
1400                  space(" ", 1)
1401                )
1402              )
1403            )
1404          )
1405        )
1406      )
1407    )
1408  )
1409
1410def separator(a: Tree, +b: Tree) -> Doc:
1411  separator.of(last(a), first(b), Bool.not(is_leaf(b)), b)
1412
1413# "#x" gets a space; empty comments, whitespace, test expectations (#|),
1414# directives (#!) and headings (##) stay as they are.
1415def comment_text.after(rest: String, +keep: String) -> String:
1416  match rest:
1417    case SNil{}:
1418      "#"
1419    case SCon{+c, more}:
1420      Bool.pick(
1421        String,
1422        is_blank(c) || Char.is_eq(c, '#') || Char.is_eq(c, '!') || Char.is_eq(c, '|'),
1423        "#" ++ keep,
1424        "# " ++ keep
1425      )
1426
1427def comment_text(text: String, keep: String) -> String:
1428  match text:
1429    case SCon{'#', +rest}:
1430      comment_text.after(rest, rest)
1431    case _:
1432      keep
1433
1434def leaf_doc(+t: Token) -> Doc:
1435  match t:
1436    case Token{text, beg, end, col, Newline{}, sym, name, tail, from}:
1437      nothing()
1438    case Token{+text, beg, end, col, Comment{}, sym, name, tail, from}:
1439      str(comment_text(text, text))
1440    case Token{text, beg, end, col, Literal{}, sym, name, tail, from}:
1441      str(text)
1442    case Token{text, beg, end, col, Code{}, sym, name, tail, from}:
1443      code(t)
1444
1445# Levels
1446# ------
1447def should_pop(levels: List<&2, U32>, col: U32) -> Bool:
1448  match levels:
1449    case Con{top, Con{x, xs}}:
1450      U32.is_lt(col, top)
1451    case _:
1452      False{}
1453
1454def pop_levels(levels: List<&2, U32>, +col: U32, go: Bool) -> List<&2, U32>:
1455  match levels go:
1456    case Con{top, +more} True{}:
1457      pop_levels(more, col, should_pop(more, col))
1458    case Con{top, more} False{}:
1459      Con{top, more}
1460    case Nil{} _:
1461      Nil{}
1462
1463def deepen(levels: List<&2, U32>, +col: U32) -> List<&2, U32>:
1464  match levels:
1465    case Nil{}:
1466      [col]
1467    case Con{+top, +more}:
1468      Bool.pick(List<&2, U32>, U32.is_gt(col, top), Con{col, Con{top, more}}, Con{top, more})
1469
1470# Closes the levels deeper than col, then opens col if it is deeper.
1471def indented(+levels: List<&2, U32>, +col: U32) -> List<&2, U32>:
1472  deepen(pop_levels(levels, col, should_pop(levels, col)), col)
1473
1474def depth.go(levels: List<&2, U32>, n: U32) -> U32:
1475  match levels:
1476    case Nil{}:
1477      n
1478    case Con{top, more}:
1479      depth.go(more, (n + 1: U32))
1480
1481def depth(levels: List<&2, U32>) -> U32:
1482  (depth.go(levels, 0) - 1: U32)
1483
1484# Sequences
1485# ---------
1486def fresh(base: U32) -> Seq:
1487  Seq{[], None{}, None{}, [base], False{}}
1488
1489def Seq.put(s: Seq, d: Doc) -> Seq:
1490  match s:
1491    case Seq{out, None{}, prev, levels, skip}:
1492      Seq{Con{d, out}, None{}, prev, levels, skip}
1493    case Seq{out, Some{chain}, prev, levels, skip}:
1494      Seq{out, Some{Con{d, chain}}, prev, levels, skip}
1495
1496def Seq.chain(s: Seq) -> Seq:
1497  match s:
1498    case Seq{out, None{}, prev, levels, skip}:
1499      Seq{out, Some{[soft(" ", 1)]}, prev, levels, skip}
1500    case Seq{out, Some{chain}, prev, levels, skip}:
1501      Seq{out, Some{Con{soft(" ", 1), chain}}, prev, levels, skip}
1502
1503def Seq.doc(s: Seq, tab: U32) -> Doc:
1504  match s:
1505    case Seq{out, None{}, prev, levels, skip}:
1506      grp(cat_rev(out))
1507    case Seq{out, Some{chain}, prev, levels, skip}:
1508      grp(cat_rev(Con{nest(cat_rev(chain), tab), out}))
1509
1510def is_prev_comment(prev: Maybe<&2, Tree>) -> Bool:
1511  match prev:
1512    case Some{p}:
1513      is_comment(last(p))
1514    case None{}:
1515      False{}
1516
1517# A run of newlines breaks the line at the next tree's level.
1518def Seq.level(
1519  +levels: List<&2, U32>,
1520  tab: U32,
1521  out: List<&2, Doc>,
1522  chain: Maybe<&2, List<&2, Doc>>
1523) -> Seq:
1524  Seq.put(Seq{out, chain, None{}, levels, True{}}, hard((depth(levels) * tab: U32)))
1525
1526def Seq.end(comment: Bool, s: Seq) -> Seq:
1527  match comment:
1528    case True{}:
1529      Seq.put(s, hard(0))
1530    case False{}:
1531      s
1532
1533def Seq.break(up: Maybe<&2, Tree>, tab: U32, s: Seq) -> Seq:
1534  match up s:
1535    case Some{u} Seq{out, chain, prev, levels, skip}:
1536      Seq.level(indented(levels, Token.col(first(u))), tab, out, chain)
1537    case None{} Seq{out, chain, prev, levels, skip}:
1538      Seq.end(is_prev_comment(prev), Seq{out, chain, None{}, levels, True{}})
1539
1540def Seq.newline.if(trigger: Bool, rest: List<&2, Tree>, tab: U32, s: Seq) -> Seq:
1541  match trigger:
1542    case True{}:
1543      Seq.break(upcoming(rest), tab, s)
1544    case False{}:
1545      s
1546
1547def Seq.newline(rest: List<&2, Tree>, tab: U32, raw: Bool, s: Seq) -> Seq:
1548  match s:
1549    case Seq{out, chain, prev, levels, True{}}:
1550      Seq{out, chain, prev, levels, True{}}
1551    case Seq{out, chain, +prev, levels, False{}}:
1552      Seq.newline.if(
1553        raw || is_prev_comment(prev),
1554        rest,
1555        tab,
1556        Seq{out, chain, prev, levels, False{}}
1557      )
1558
1559def Seq.joint(chain: Bool, p: Tree, n: Tree, s: Seq) -> Seq:
1560  match chain:
1561    case True{}:
1562      Seq.chain(s)
1563    case False{}:
1564      Seq.put(s, separator(p, n))
1565
1566def Seq.tree(+n: Tree, d: Doc, next: Maybe<&2, Tree>, raw: Bool, s: Seq) -> Seq:
1567  match s:
1568    case Seq{out, chain, None{}, levels, skip}:
1569      Seq.put(Seq{out, chain, Some{n}, levels, False{}}, d)
1570    case Seq{out, chain, Some{p}, levels, skip}:
1571      Seq.put(
1572        Seq.joint(
1573          Bool.not(
1574            raw
1575          )
1576            && is_leaf(n)
1577            && is_operator(Token.sym(first(n)))
1578            && Bool.not(is_glued_prefix(first(n), next)),
1579          p,
1580          n,
1581          Seq{out, chain, Some{n}, levels, False{}}
1582        ),
1583        d
1584      )
1585
1586def Seq.step(
1587  newline: Bool,
1588  n: Tree,
1589  d: Doc,
1590  rest: List<&2, Tree>,
1591  tab: U32,
1592  raw: Bool,
1593  s: Seq
1594) -> Seq:
1595  match newline:
1596    case True{}:
1597      Seq.newline(rest, tab, raw, s)
1598    case False{}:
1599      Seq.tree(n, d, head(rest), raw, s)
1600
1601def Seq.visit(+n: Tree, d: Doc, rest: List<&2, Tree>, tab: U32, raw: Bool, s: Seq) -> Seq:
1602  Seq.step(is_newline(n), n, d, rest, tab, raw, s)
1603
1604# Parts
1605# -----
1606def Split.fresh() -> Split:
1607  Split{[], True{}, False{}, False{}, False{}}
1608
1609def Split.track(+n: Tree, attach: Bool, sp: Split) -> Split:
1610  match sp:
1611    case Split{parts, newlines, comment, a, comments}:
1612      Split{
1613        parts,
1614        newlines && is_newline(n),
1615        Bool.pick(Bool, is_newline(n), comment, is_comment(last(n))),
1616        attach,
1617        comments
1618      }
1619
1620def Split.end(tab: U32, base: U32, st: State) -> State:
1621  match st:
1622    case State{seq, Split{parts, newlines, comment, attach, comments}}:
1623      State{
1624        fresh(base),
1625        Split{
1626          Con{Part{Seq.doc(seq, tab), newlines, comment}, parts},
1627          True{},
1628          False{},
1629          False{},
1630          comments
1631        }
1632      }
1633
1634def Split.comments(n: Tree, sp: Split) -> Split:
1635  match sp:
1636    case Split{parts, newlines, comment, attach, comments}:
1637      Split{parts, newlines, comment, attach, comments || is_comment(first(n))}
1638
1639def has_head_comment(xs: List<&2, Tree>) -> Bool:
1640  match xs:
1641    case Con{x, rest}:
1642      is_comment(first(x))
1643    case Nil{}:
1644      False{}
1645
1646def Split.act(n: Tree, rest: List<&2, Tree>, sp: Split) -> Act:
1647  match sp:
1648    case Split{parts, newlines, comment, True{}, comments}:
1649      AddEnd{}
1650    case Split{parts, newlines, comment, False{}, comments}:
1651      Bool.pick(
1652        Act,
1653        is_leaf_of(n, Comma{}),
1654        Bool.pick(Act, has_head_comment(rest), AddAttach{}, End{}),
1655        Add{}
1656      )
1657
1658def Split.apply(
1659  act: Act,
1660  +n: Tree,
1661  d: Doc,
1662  rest: List<&2, Tree>,
1663  +tab: U32,
1664  base: U32,
1665  st: State
1666) -> State:
1667  match act st:
1668    case Add{} State{seq, sp}:
1669      State{Seq.visit(n, d, rest, tab, False{}, seq), Split.track(n, False{}, sp)}
1670    case AddAttach{} State{seq, sp}:
1671      State{Seq.visit(n, d, rest, tab, False{}, seq), Split.track(n, True{}, sp)}
1672    case AddEnd{} State{seq, sp}:
1673      Split.end(
1674        tab,
1675        base,
1676        State{Seq.visit(n, d, rest, tab, False{}, seq), Split.track(n, False{}, sp)}
1677      )
1678    case End{} State{seq, sp}:
1679      Split.end(tab, base, State{seq, sp})
1680
1681def Split.visit(+n: Tree, d: Doc, +rest: List<&2, Tree>, tab: U32, base: U32, st: State) -> State:
1682  match st:
1683    case State{seq, +sp}:
1684      Split.apply(Split.act(n, rest, sp), n, d, rest, tab, base, State{seq, Split.comments(n, sp)})
1685
1686def should_drop(parts: List<&2, Part>) -> Bool:
1687  match parts:
1688    case Con{Part{d, True{}, c}, Con{p, ps}}:
1689      True{}
1690    case _:
1691      False{}
1692
1693# Drops the trailing parts that hold only newlines, keeping one part.
1694def drop_parts(parts: List<&2, Part>, go: Bool) -> List<&2, Part>:
1695  match parts go:
1696    case Con{p, +rest} True{}:
1697      drop_parts(rest, should_drop(rest))
1698    case ps False{}:
1699      ps
1700    case Nil{} _:
1701      Nil{}
1702
1703def content.sep(comment: Bool, acc: List<&2, Doc>) -> List<&2, Doc>:
1704  match comment:
1705    case True{}:
1706      Con{hard(0), acc}
1707    case False{}:
1708      Con{soft(" ", 1), Con{text(",", 1), acc}}
1709
1710def content.go(parts: List<&2, Part>, comment: Bool, acc: List<&2, Doc>) -> List<&2, Doc>:
1711  match parts:
1712    case Nil{}:
1713      acc
1714    case Con{Part{d, newlines, next}, rest}:
1715      content.go(rest, next, Con{d, content.sep(comment, acc)})
1716
1717# The parts' docs with their separators, last first.
1718def content(parts: List<&2, Part>) -> List<&2, Doc>:
1719  match parts:
1720    case Nil{}:
1721      []
1722    case Con{Part{d, newlines, comment}, rest}:
1723      content.go(rest, comment, [d])
1724
1725def opening(comments: Bool) -> Doc:
1726  match comments:
1727    case True{}:
1728      hard(0)
1729    case False{}:
1730      soft("", 0)
1731
1732# A generic's closing > stays glued to the term before it: Bend parses a
1733# spaced > as a comparison, even across a newline.
1734def closing(close: Token, comments: Bool) -> Doc:
1735  Bool.pick(Doc, is_sym(Token.sym(close), Gt{}), nothing(), opening(comments))
1736
1737def Split.doc(open: Token, +close: Token, tab: U32, st: State) -> Doc:
1738  match st:
1739    case State{seq, Split{+parts, newlines, comment, attach, +comments}}:
1740      grp(
1741        cat(
1742          [
1743            code(open),
1744            nest(
1745              cat(
1746                [
1747                  opening(comments),
1748                  cat_rev(content(List.reverse(&2, Part, drop_parts(parts, should_drop(parts)))))
1749                ]
1750              ),
1751              tab
1752            ),
1753            closing(close, comments),
1754            code(close)
1755          ]
1756        )
1757      )
1758
1759# Build
1760# -----
1761def mode_of(open: Token, close: Token, children: List<&2, Tree>, block: Bool) -> Mode:
1762  match children block:
1763    case Nil{} _:
1764      Bare{open, close}
1765    case _ True{}:
1766      Block{open, close}
1767    case _ False{}:
1768      Parts{open, close}
1769
1770def start(base: U32) -> State:
1771  State{fresh(base), Split.fresh()}
1772
1773def visit(
1774  +mode: Mode,
1775  n: Tree,
1776  d: Doc,
1777  rest: List<&2, Tree>,
1778  tab: U32,
1779  base: U32,
1780  st: State
1781) -> State:
1782  match mode st:
1783    case Plain{} State{seq, sp}:
1784      State{Seq.visit(n, d, rest, tab, False{}, seq), sp}
1785    case Block{open, close} State{seq, sp}:
1786      State{Seq.visit(n, d, rest, tab, True{}, seq), sp}
1787    case Parts{open, close} st2:
1788      Split.visit(n, d, rest, tab, base, st2)
1789    case Bare{open, close} st2:
1790      st2
1791
1792def finish(mode: Mode, st: State, +tab: U32, base: U32) -> Doc:
1793  match mode st:
1794    case Plain{} State{seq, sp}:
1795      Seq.doc(seq, tab)
1796    case Block{open, close} State{seq, sp}:
1797      cat([code(open), Seq.doc(seq, tab), code(close)])
1798    case Parts{open, close} st2:
1799      Split.doc(open, close, tab, Split.end(tab, base, st2))
1800    case Bare{open, close} st2:
1801      cat([code(open), code(close)])
1802
1803# The doc of a list of trees, read as mode says. A group's children are
1804# built first, by the same def, as a smaller part of the list.
1805def build(nodes: List<&2, Tree>, +tab: U32, +base: U32, +mode: Mode, st: State) -> Doc:
1806  match nodes:
1807    case Nil{}:
1808      finish(mode, st, tab, base)
1809    case Con{Leaf{+t}, +rest}:
1810      build(rest, tab, base, mode, visit(mode, Leaf{t}, leaf_doc(t), rest, tab, base, st))
1811    case Con{Group{+open, +close, +children, +block}, +rest}:
1812      build(
1813        rest,
1814        tab,
1815        base,
1816        mode,
1817        visit(
1818          mode,
1819          Group{open, close, children, block},
1820          build(children, tab, base, mode_of(open, close, children, block), start(base)),
1821          rest,
1822          tab,
1823          base,
1824          st
1825        )
1826      )
1827
1828def sequence(nodes: List<&2, Tree>, tab: U32, +base: U32) -> Doc:
1829  build(nodes, tab, base, Plain{}, start(base))
1830
1831# Printer
1832# -------
1833# Jobs for a Cat's docs, last first, so its first doc ends on top.
1834def push(docs: List<&2, Doc>, +indent: U32, +flat: Bool, jobs: List<&2, Job>) -> List<&2, Job>:
1835  match docs:
1836    case Nil{}:
1837      jobs
1838    case Con{d, rest}:
1839      push(rest, indent, flat, Con{Job{d, indent, flat}, jobs})
1840
1841def fits.flat.of(over: Bool, brk: Bool, jobs: List<&2, Job>, left: U32) -> Fit:
1842  match over brk:
1843    case True{} _:
1844      Stop{False{}}
1845    case False{} True{}:
1846      Stop{True{}}
1847    case False{} False{}:
1848      Go{jobs, left}
1849
1850# A doc printed flat, whole: past the width, or up to a break within it.
1851def fits.flat(f: Flat, jobs: List<&2, Job>, +left: U32) -> Fit:
1852  match f:
1853    case Flat{+w, brk}:
1854      fits.flat.of(U32.is_gt(w, left), brk, jobs, (left - w: U32))
1855
1856def fits.width(stop: Bool, +n: U32, jobs: List<&2, Job>, +left: U32) -> Fit:
1857  match stop:
1858    case True{}:
1859      Stop{True{}}
1860    case False{}:
1861      Bool.pick(Fit, U32.is_gt(n, left), Stop{False{}}, Go{jobs, (left - n: U32)})
1862
1863def fits.step(jobs: List<&2, Job>, left: U32) -> Fit:
1864  match jobs:
1865    case Nil{}:
1866      Stop{True{}}
1867    case Con{Job{Str{body, blank, width, tail}, indent, flat}, rest}:
1868      fits.width(Maybe.is_some(&2, U32, tail), width, rest, left)
1869    case Con{Job{Line{text, width, hard, offset}, indent, flat}, rest}:
1870      fits.width(hard || Bool.not(flat), width, rest, left)
1871    case Con{Job{Grp{doc, f}, indent, flat}, rest}:
1872      fits.flat(f, rest, left)
1873    case Con{Job{Nest{doc, amount, f}, indent, flat}, rest}:
1874      fits.flat(f, rest, left)
1875    case Con{Job{Cat{docs, f}, indent, True{}}, rest}:
1876      fits.flat(f, rest, left)
1877    case Con{Job{Cat{docs, f}, indent, False{}}, rest}:
1878      Go{push(docs, indent, False{}, rest), left}
1879
1880def fits.go(fuel: Nat, f: Fit) -> Bool:
1881  match fuel f:
1882    case _ Stop{b}:
1883      b
1884    case 0n Go{jobs, left}:
1885      True{}
1886    case 1n+k Go{jobs, left}:
1887      fits.go(k, fits.step(jobs, left))
1888
1889def fits.from(over: Bool, fuel: Nat, left: U32, jobs: List<&2, Job>) -> Bool:
1890  match over:
1891    case True{}:
1892      False{}
1893    case False{}:
1894      fits.go(fuel, Go{jobs, left})
1895
1896# Whether the jobs fit before their first break, from col.
1897def fits(fuel: Nat, +width: U32, +col: U32, jobs: List<&2, Job>) -> Bool:
1898  fits.from(U32.is_gt(col, width), fuel, (width - col: U32), jobs)
1899
1900def col_of(tail: Maybe<&2, U32>, col: U32, width: U32) -> U32:
1901  match tail:
1902    case Some{t}:
1903      t
1904    case None{}:
1905      (col + width: U32)
1906
1907# Text goes out after the pending whitespace; its own trailing whitespace
1908# becomes pending.
1909def print.str(
1910  body: String,
1911  keep: String,
1912  blank: String,
1913  jobs: List<&2, Job>,
1914  out: List<&2, String>,
1915  pending: String,
1916  col: U32
1917) -> Print:
1918  match body:
1919    case SNil{}:
1920      Print{jobs, out, pending ++ blank, col}
1921    case _:
1922      Print{jobs, Con{keep, Con{pending, out}}, blank, col}
1923
1924def print.line(
1925  flat: Bool,
1926  text: String,
1927  +at: U32,
1928  jobs: List<&2, Job>,
1929  out: List<&2, String>,
1930  pending: String,
1931  col: U32,
1932  width: U32,
1933  newline: String
1934) -> Print:
1935  match flat:
1936    case True{}:
1937      Print{jobs, out, pending ++ text, (col + width: U32)}
1938    case False{}:
1939      Print{jobs, out, newline ++ pad(U32.to_nat(at), ""), at}
1940
1941def print.is_flat(
1942  flat: Bool,
1943  fuel: Nat,
1944  +width: Maybe<&2, U32>,
1945  col: U32,
1946  jobs: List<&2, Job>
1947) -> Bool:
1948  match flat width:
1949    case True{} _:
1950      True{}
1951    case False{} None{}:
1952      True{}
1953    case False{} Some{w}:
1954      fits(fuel, w, col, jobs)
1955
1956def is_unbounded(width: Maybe<&2, U32>) -> Bool:
1957  match width:
1958    case None{}:
1959      True{}
1960    case Some{w}:
1961      False{}
1962
1963def print.step(
1964  jobs: List<&2, Job>,
1965  out: List<&2, String>,
1966  pending: String,
1967  +col: U32,
1968  width: Maybe<&2, U32>,
1969  newline: String,
1970  fuel: Nat
1971) -> Print:
1972  match jobs:
1973    case Nil{}:
1974      Print{Nil{}, out, pending, col}
1975    case Con{Job{Str{+body, blank, w, tail}, indent, flat}, rest}:
1976      print.str(body, body, blank, rest, out, pending, col_of(tail, col, w))
1977    case Con{Job{Line{text, w, hard, offset}, +indent, +flat}, rest}:
1978      print.line(
1979        Bool.not(hard) && (flat || is_unbounded(width)),
1980        text,
1981        (indent + offset: U32),
1982        rest,
1983        out,
1984        pending,
1985        col,
1986        w,
1987        newline
1988      )
1989    case Con{Job{Nest{doc, amount, f}, indent, flat}, rest}:
1990      Print{Con{Job{doc, (indent + amount: U32), flat}, rest}, out, pending, col}
1991    case Con{Job{Grp{+doc, f}, +indent, +flat}, +rest}:
1992      Print{
1993        Con{
1994          Job{
1995            doc,
1996            indent,
1997            print.is_flat(flat, fuel, width, col, Con{Job{doc, indent, True{}}, rest})
1998          },
1999          rest
2000        },
2001        out,
2002        pending,
2003        col
2004      }
2005    case Con{Job{Cat{docs, f}, indent, flat}, rest}:
2006      Print{push(docs, indent, flat, rest), out, pending, col}
2007
2008# The text so far; pending whitespace is never written at the end.
2009def print.go(fuel: Nat, p: Print, +width: Maybe<&2, U32>, +newline: String, +budget: Nat) -> String:
2010  match fuel p:
2011    case _ Print{Nil{}, out, pending, col}:
2012      Strings.onto(out, "")
2013    case 0n Print{jobs, out, pending, col}:
2014      Strings.onto(out, "")
2015    case 1n+k Print{jobs, out, pending, col}:
2016      print.go(
2017        k,
2018        print.step(jobs, out, pending, col, width, newline, budget),
2019        width,
2020        newline,
2021        budget
2022      )
2023
2024# The doc at indent; fuel bounds the steps of the printer and of each fit.
2025# A group is all flat if it fits; else its separators break together.
2026# Nesting is relative, never aligned under a function name. The width is a
2027# target, not a license to split atoms.
2028def print(doc: Doc, width: Maybe<&2, U32>, +indent: U32, newline: String, +fuel: Nat) -> String:
2029  print.go(
2030    fuel,
2031    Print{[Job{doc, indent, False{}}], [], pad(U32.to_nat(indent), ""), indent},
2032    width,
2033    newline,
2034    fuel
2035  )
2036
2037# Records
2038# -------
2039def joins.of(+p: Tree, +next: Tree, following: Maybe<&2, Tree>) -> Bool:
2040  Bool.not(
2041    is_comment(last(p))
2042  )
2043    && Bool.not(is_comment(first(next)))
2044    && (
2045      (
2046        Bool.not(
2047          is_glued_prefix(first(next), following)
2048        )
2049          && is_leaf(next)
2050          && is_operator(Token.sym(first(next)))
2051      )
2052        || is_continued(Token.sym(last(p)))
2053    )
2054
2055# Whether a newline between previous and rest continues the record.
2056def joins(previous: Maybe<&2, Tree>, rest: List<&2, Tree>) -> Bool:
2057  match previous rest:
2058    case Some{p} Con{next, more}:
2059      joins.of(p, next, head(more))
2060    case _ _:
2061      False{}
2062
2063def records.newline(join: Bool, done: List<&2, List<&2, Tree>>, current: List<&2, Tree>) -> Recs:
2064  match join:
2065    case True{}:
2066      Recs{done, current}
2067    case False{}:
2068      Recs{Con{List.reverse(&2, Tree, current), done}, Nil{}}
2069
2070def records.step(
2071  newline: Bool,
2072  n: Tree,
2073  rest: List<&2, Tree>,
2074  done: List<&2, List<&2, Tree>>,
2075  +current: List<&2, Tree>
2076) -> Recs:
2077  match newline:
2078    case False{}:
2079      Recs{done, Con{n, current}}
2080    case True{}:
2081      records.newline(joins(head(current), rest), done, current)
2082
2083def records(nodes: List<&2, Tree>, r: Recs) -> List<&2, List<&2, Tree>>:
2084  match nodes r:
2085    case Nil{} Recs{done, current}:
2086      List.reverse(&2, List<&2, Tree>, Con{List.reverse(&2, Tree, current), done})
2087    case Con{+n, +rest} Recs{done, current}:
2088      records(rest, records.step(is_newline(n), n, rest, done, current))
2089
2090def cut.step(+n: Tree, c: Cut) -> Cut:
2091  match c:
2092    case Cut{before, after, True{}}:
2093      Cut{before, Con{n, after}, True{}}
2094    case Cut{before, after, False{}}:
2095      Cut{Con{n, before}, after, is_leaf_of(n, Colon{})}
2096
2097def cut.go(xs: List<&2, Tree>, c: Cut) -> Cut:
2098  match xs c:
2099    case Nil{} Cut{before, after, found}:
2100      Cut{List.reverse(&2, Tree, before), List.reverse(&2, Tree, after), found}
2101    case Con{n, rest} c2:
2102      cut.go(rest, cut.step(n, c2))
2103
2104# Whether the cut has a body after its colon that is not a comment.
2105def cut.is_inline(c: Cut) -> Bool:
2106  match c:
2107    case Cut{before, Con{x, xs}, True{}}:
2108      Bool.not(is_comment(first(x)))
2109    case _:
2110      False{}
2111
2112def is_not_hash(s: String) -> Bool:
2113  match s:
2114    case SNil{}:
2115      False{}
2116    case SCon{'#', rest}:
2117      False{}
2118    case _:
2119      True{}
2120
2121def split_hash.of(r: Run) -> Hash:
2122  match r:
2123    case Run{before, n, SNil{}, last}:
2124      Hash{before, None{}}
2125    case Run{before, n, rest, last}:
2126      Hash{before, Some{rest}}
2127
2128def split_hash(+s: String) -> Hash:
2129  split_hash.of(scan(~is_not_hash, s, s, is_not_hash(s), "", 0, '\0'))
2130
2131def import_space.of(+rest: String, s: String) -> String:
2132  Bool.pick(String, head_passes(~is_blank, rest), "import " ++ drop_blank(rest), s)
2133
2134def import_space(+s: String) -> String:
2135  import_space.of(String.drop(s, 6n), s)
2136
2137def is_alias_at.as(s: String) -> Bool:
2138  match s:
2139    case SCon{'a', SCon{'s', rest}}:
2140      head_passes(~is_blank, rest)
2141    case _:
2142      False{}
2143
2144def is_alias_at(+s: String) -> Bool:
2145  head_passes(~is_blank, s) && is_alias_at.as(drop_blank(s))
2146
2147def alias.go(s: String, hit: Bool, acc: String) -> String:
2148  match s hit:
2149    case SNil{} _:
2150      String.reverse(acc)
2151    case SCon{c, rest} True{}:
2152      String.reverse(acc) ++ " as " ++ drop_blank(String.drop(drop_blank(SCon{c, rest}), 2n))
2153    case SCon{c, +rest} False{}:
2154      alias.go(rest, is_alias_at(rest), SCon{c, acc})
2155
2156def alias(+s: String) -> String:
2157  alias.go(s, is_alias_at(s), "")
2158
2159def import_doc.of(h: Hash) -> Doc:
2160  match h:
2161    case Hash{before, None{}}:
2162      str(alias(import_space(trim_end(before))))
2163    case Hash{before, Some{+comment}}:
2164      cat(
2165        [
2166          str(alias(import_space(trim_end(before)))),
2167          space("  ", 2),
2168          str(comment_text(comment, comment))
2169        ]
2170      )
2171
2172# Where the record that head starts ends.
2173def record_end(head: Token, record: List<&2, Tree>) -> U32:
2174  Token.end(last(Maybe.default(&2, Tree, last_of(record, None{}), Leaf{head})))
2175
2176# Paths and aliases have their own grammar: / and - are not operators there,
2177# so an import keeps its text and only its spacing changes.
2178def import_doc(+head: Token, record: List<&2, Tree>) -> Doc:
2179  import_doc.of(
2180    split_hash(
2181      trim_end(
2182        Strings.take(
2183          Token.from(head),
2184          U32.to_nat((record_end(head, record) - Token.beg(head): U32))
2185        )
2186      )
2187    )
2188  )
2189
2190def decl_doc.of(inline: Bool, c: Cut, record: List<&2, Tree>, +tab: U32, +col: U32) -> Doc:
2191  match inline c:
2192    case True{} Cut{before, after, found}:
2193      cat([sequence(before, tab, col), nest(cat([hard(0), sequence(after, tab, col)]), tab)])
2194    case False{} c2:
2195      sequence(record, tab, col)
2196
2197def decl_doc.cut(+c: Cut, record: List<&2, Tree>, declaration: Bool, tab: U32, +col: U32) -> Doc:
2198  decl_doc.of(U32.is_zero(col) && declaration && cut.is_inline(c), c, record, tab, col)
2199
2200# A declaration's body on the line of its colon moves to the next line,
2201# indented.
2202def decl_doc(+record: List<&2, Tree>, declaration: Bool, tab: U32, col: U32) -> Doc:
2203  decl_doc.cut(cut.go(record, Cut{[], [], False{}}), record, declaration, tab, col)
2204
2205def record_doc(
2206  is_import: Bool,
2207  head: Token,
2208  record: List<&2, Tree>,
2209  declaration: Bool,
2210  tab: U32,
2211  col: U32
2212) -> Doc:
2213  match is_import:
2214    case True{}:
2215      import_doc(head, record)
2216    case False{}:
2217      decl_doc(record, declaration, tab, col)
2218
2219# The text of the first tokens of `xs`, after `acc`, the texts so far, last first.
2220def joined(xs: List<&2, Tree>, acc: List<&2, String>) -> String:
2221  match xs:
2222    case Nil{}:
2223      Strings.onto(acc, "")
2224    case Con{x, rest}:
2225      joined(rest, Token.text(first(x)) <> acc)
2226
2227def is_unsafe(at: Bool, record: List<&2, Tree>) -> Bool:
2228  match at:
2229    case True{}:
2230      is_same(joined(record, []), "@unsafe")
2231    case False{}:
2232      False{}
2233
2234def gapped(gap: Bool, rows: List<&2, String>) -> List<&2, String>:
2235  match gap rows:
2236    case True{} Con{+row, +rest}:
2237      Bool.pick(List<&2, String>, String.is_empty(row), Con{row, rest}, Con{"", Con{row, rest}})
2238    case _ rows2:
2239      rows2
2240
2241def rows.line(+head: Token, +record: List<&2, Tree>, +set: Settings, r: Rows) -> Rows:
2242  match set r:
2243    case Settings{+tab, width, newline} Rows{levels, +blank, unsafe, prior, rows}:
2244      +col = Token.col(head)
2245      +levels2 = indented(levels, col)
2246      +declaration = is_declaration(Token.sym(head))
2247      +comment = is_comment(head)
2248      gap = Bool.pick(
2249        Bool,
2250        U32.is_zero(col),
2251        (declaration && Bool.not(prior) && Bool.not(unsafe)) || (comment && blank),
2252        blank
2253      )
2254      doc = record_doc(is_import(Token.sym(head)), head, record, declaration, tab, col)
2255      fuel = U32.to_nat(
2256        (fuel_per_point() * ((record_end(head, record) - Token.beg(head): U32) + 1): U32)
2257      )
2258      Rows{
2259        levels2,
2260        False{},
2261        is_unsafe(is_sym(Token.sym(head), At{}), record),
2262        comment,
2263        Con{print(doc, width, (depth(levels2) * tab: U32), newline, fuel), gapped(gap, rows)}
2264      }
2265
2266def rows.step(record: List<&2, Tree>, set: Settings, r: Rows) -> Rows:
2267  match record r:
2268    case Nil{} Rows{levels, blank, unsafe, comment, rows}:
2269      Rows{levels, True{}, unsafe, comment, rows}
2270    case Con{+x, xs} r2:
2271      rows.line(first(x), Con{x, xs}, set, r2)
2272
2273def rows.go(records: List<&2, List<&2, Tree>>, +set: Settings, r: Rows) -> Rows:
2274  match records:
2275    case Nil{}:
2276      r
2277    case Con{record, rest}:
2278      rows.go(rest, set, rows.step(record, set, r))
2279
2280def is_head_empty(rows: List<&2, String>) -> Bool:
2281  match rows:
2282    case Con{row, rest}:
2283      String.is_empty(row)
2284    case Nil{}:
2285      False{}
2286
2287def drop_empty(rows: List<&2, String>, go: Bool) -> List<&2, String>:
2288  match rows go:
2289    case Con{row, +rest} True{}:
2290      drop_empty(rest, is_head_empty(rest))
2291    case rest _:
2292      rest
2293
2294def join(rows: List<&2, String>, +newline: String, acc: String) -> String:
2295  match rows:
2296    case Nil{}:
2297      acc
2298    case Con{row, rest}:
2299      join(rest, newline, row ++ newline ++ acc)
2300
2301# The rows, last first, joined, without the empty rows at the end, with a
2302# newline. Each row has no trailing whitespace already.
2303def text_of.trimmed(rows: List<&2, String>, +newline: String) -> String:
2304  match rows:
2305    case Nil{}:
2306      newline
2307    case Con{row, rest}:
2308      join(rest, newline, row ++ newline)
2309
2310def text_of(+rows: List<&2, String>, newline: String) -> String:
2311  text_of.trimmed(drop_empty(rows, is_head_empty(rows)), newline)
2312
2313def format.text(r: Rows, newline: String) -> String:
2314  match r:
2315    case Rows{levels, blank, unsafe, comment, rows}:
2316      text_of(rows, newline)
2317
2318def format.of(nodes: Maybe<&2, List<&2, Tree>>, +set: Settings) -> Maybe<&2, String>:
2319  match nodes set:
2320    case None{} s:
2321      None{}
2322    case Some{ns} Settings{tab, width, +newline}:
2323      Some{
2324        format.text(
2325          rows.go(
2326            records(ns, Recs{[], []}),
2327            Settings{tab, width, newline},
2328            Rows{[0], False{}, False{}, False{}, []}
2329          ),
2330          newline
2331        )
2332      }
2333
2334# The formatted text, or None{} if its delimiters do not balance.
2335def format(source: String, set: Settings) -> Maybe<&2, String>:
2336  format.of(tree.go(tokens(source), None{}, Built{[], []}), set)
2337
2338# Rule
2339# ====
2340
2341# Where `a` and `b` first differ: `same` is whether the characters before `at` were the same.
2342def differ(a: String, b: String, at: U32, same: Bool) -> U32:
2343  match a b same:
2344    case _ _ False{}:
2345      (at - 1: U32)
2346    case SCon{x, xs} SCon{y, ys} True{}:
2347      differ(xs, ys, (at + 1: U32), Char.is_eq(x, y))
2348    case _ _ True{}:
2349      at
2350
2351# Where the line that `s` starts, at `i`, ends: before its newline, or at the end of the text.
2352def line_end(s: String, i: U32) -> U32:
2353  match s:
2354    case SNil{}:
2355      i
2356    case SCon{'\n', rest}:
2357      i
2358    case SCon{c, rest}:
2359      line_end(rest, (i + 1: U32))
2360
2361def judged(same: Bool, +out: String, +text: String, +path: String) -> List<&2, Lint.Finding>:
2362  match same:
2363    case True{}:
2364      []
2365    case False{}:
2366      +at = differ(out, text, 0, True{})
2367      [
2368        Lint.Finding{
2369          Lint.Warning{},
2370          Lint.Span{path, at, line_end(String.drop(text, U32.to_nat(at)), at)},
2371          "Apply canonical formatting.",
2372          [
2373            Lint.Fix{
2374              "Format the file",
2375              [Lint.Replace{Lint.Span{path, 0, U32.from_nat(String.length(text))}, out}]
2376            }
2377          ]
2378        }
2379      ]
2380
2381def judge(formatted: Maybe<&2, String>, +text: String, path: String) -> List<&2, Lint.Finding>:
2382  match formatted:
2383    case None{}:
2384      [
2385        Lint.Finding{
2386          Lint.Warning{},
2387          Lint.Span{path, 0, 0},
2388          "Cannot format this file: unbalanced delimiters.",
2389          []
2390        }
2391      ]
2392    case Some{+out}:
2393      judged(is_same(out, text), out, text, path)
2394
2395def first_newline(s: String) -> String:
2396  match s:
2397    case SNil{}:
2398      "\n"
2399    case SCon{'\r', SCon{'\n', rest}}:
2400      "\r\n"
2401    case SCon{'\n', rest}:
2402      "\n"
2403    case SCon{c, rest}:
2404      first_newline(rest)
2405
2406def newline.preserve(preserve: Bool, source: String) -> String:
2407  match preserve:
2408    case True{}:
2409      first_newline(source)
2410    case False{}:
2411      "\n"
2412
2413def newline.crlf(crlf: Bool, eol: String, source: String) -> String:
2414  match crlf:
2415    case True{}:
2416      "\r\n"
2417    case False{}:
2418      newline.preserve(String.eq(eol, "preserve"), source)
2419
2420def newline_of(+eol: String, source: String) -> String:
2421  newline.crlf(String.eq(eol, "crlf"), eol, source)
2422
2423def width_of(value: Maybe<&2, Lint.Value>) -> Maybe<&2, U32>:
2424  match value:
2425    case Some{Lint.Num{n}}:
2426      Some{n}
2427    case _:
2428      None{}
2429
2430def settings(+here: Lint.Here) -> Settings:
2431  Settings{
2432    Lint.Value.number(Lint.Here.option(here, "tabWidth")),
2433    width_of(Lint.Here.option(here, "wrapAtWidth")),
2434    newline_of(Lint.Value.text(Lint.Here.option(here, "endOfLine")), Lint.Here.text(here))
2435  }
2436
2437def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
2438  Lint.done(
2439    judge(format(Lint.Here.text(here), settings(here)), Lint.Here.text(here), Lint.Here.path(here))
2440  )
2441
2442def options() -> List<&2, Lint.Declared>:
2443  [
2444    Lint.Declared{"tabWidth", Lint.Num{2}, Lint.NumberOption{1, 4294967295}},
2445    Lint.Declared{
2446      "wrapAtWidth",
2447      Lint.Num{100},
2448      Lint.AnyOption{[Lint.NumberOption{1, 4294967295}, Lint.TextOption{["never"]}]}
2449    },
2450    Lint.Declared{"endOfLine", Lint.Chars{"lf"}, Lint.TextOption{["lf", "crlf", "preserve"]}}
2451  ]
2452
2453def rule() -> Lint.Rule:
2454  Lint.Rule{Lint.Needs{False{}, False{}}, options(), Lint.OnFile{run}}