WokML compiler code coverage report (LCOV)
Current view: top level - c - wok_print.c (source / functions) Coverage Total Hit
Test: 084deda Lines: 95.5 % 1103 1053
Test Date: 2026-08-10 11:39:41 Functions: 95.4 % 65 62
Legend: Lines:     hit not hit

            Line data    Source code
       1              : // wok_print -- see wok_print.h for the contract and the canonical decisions.
       2              : //
       3              : // The printer INVERTS wok_parse.c production by production, so its three
       4              : // precedence ladders are copies of that file's call graph, not a second
       5              : // opinion about it:
       6              : //
       7              : //   type > arrow > typeapp > atomtype
       8              : //   expr > chain  > app     > atom
       9              : //   pat  > patapp > atompat
      10              : //
      11              : // Two facts about the language shape everything here.
      12              : //
      13              : // FACT ONE: a block is opened by COLUMN, so only the LAST thing on a line
      14              : // may open one. Every construct with a continuation lead after a body
      15              : // (`in`, `then`, `else`, `where`) therefore has two shapes, and which one is
      16              : // used is decided by opens_block() on the part that precedes the lead --
      17              : // never by a width budget.
      18              : //
      19              : // FACT TWO: inside brackets layout is suspended (L2), so a newline emitted
      20              : // there produces NO layout token and a block written there would silently
      21              : // flatten. `flat` marks that region: within it no BLOCK may be opened.
      22              : // Since the parser cannot BUILD an indented block inside brackets either,
      23              : // the two restrictions match exactly, and a tree that violates this could
      24              : // not have come from a parse. A FILL break is a different thing and is legal
      25              : // there -- see "line filling" below.
      26              : //
      27              : // FACT THREE, which decides the whole of line filling: a line break CHANGES
      28              : // THE PARSE. A new line whose first token is an ITEM LEAD either starts a new
      29              : // block item (the tree silently changed) or is rejected outright for being
      30              : // deeper than its block. Only two breaks are safe, and the filler makes no
      31              : // others:
      32              : //
      33              : //   * immediately before a CONTINUATION LEAD, which L4 answers with DEDENTs
      34              : //     and no separator, so the logical line continues; and
      35              : //   * anywhere inside a bracket group, where L2 suspends layout entirely.
      36              : //
      37              : // Application juxtaposition owns no separator, so it cannot be broken and a
      38              : // long application stays long. Manufacturing a break point by adding
      39              : // parentheses is deliberately NOT done: punctuation the author did not write
      40              : // is a worse trade than one long line.
      41              : 
      42              : // The prelude comes FIRST: it carries the POSIX feature-test macros, which
      43              : // have no effect once a system header has been read. wok_base.h hard-errors
      44              : // if it is reached too late.
      45              : #include "wok_base.h"
      46              : 
      47              : #include "wok_print.h"
      48              : 
      49              : #include <inttypes.h>
      50              : #include <stdint.h>
      51              : #include <string.h>
      52              : 
      53              : #include "wok_token.h"
      54              : #include "wok_trivia.h"
      55              : 
      56              : // ------------------------------------------------------------------- sink
      57              : //
      58              : // One algorithm, three destinations: a FILE, an arena-backed growable
      59              : // buffer, and a MEASURE-ONLY sink. The third is what alignment needs: the
      60              : // width of every head in a block must be known before the first head is
      61              : // written, and re-rendering into a counter is cheaper than teaching every
      62              : // head printer to report its own width.
      63              : 
      64              : typedef struct {
      65              :   bool to_file;
      66              :   bool count_only;
      67              :   FILE *file;
      68              :   WokArena *arena;
      69              :   char *buf;
      70              :   usize len, cap;
      71              : 
      72              :   const char *src;
      73              :   const char *path;     // named in an overflow report; null means <input>
      74              :   const WokTrivia *tv;  // the comments, or null for a tree that has none
      75              :   usize cols;  // columns written on the current line, in codepoints
      76              :   u32 depth;
      77              :   bool quiet;  // measuring: a fault is reported by the real pass, not twice
      78              : 
      79              :   // ---- line filling ----
      80              :   bool nofill;      // measuring a flat width: never introduce a break
      81              :   bool nl_seen;     // a line was broken during the measurement
      82              :   bool check_lead;  // the next byte written opens a FILL line
      83              :   u32 line;         // 1-based output line, for the overflow report
      84              :   bool line_over;     // this line has already crossed the width
      85              :   bool in_comment;    // writing a comment, which is copied verbatim
      86              :   bool over_comment;  // the crossing happened inside one
      87              :   u16 over_tag;  // the construct blamed for that crossing
      88              : 
      89              :   // The open block columns, innermost last. A block is the ONLY thing that
      90              :   // may hold items at a column of its own, so this is the whole of what the
      91              :   // safety property compares against.
      92              :   u16 blk[WOK_PRINT_BLOCK_MAX];
      93              :   int nblk;
      94              : 
      95              :   // The nodes currently being written, outermost first. Read only to NAME
      96              :   // the construct in an overflow report.
      97              :   u16 tstack[WOK_PRINT_MAX_DEPTH];
      98              :   int tn;
      99              : } Pr;
     100              : 
     101              : // The two latches; see wok_print.h for why they are process-wide.
     102              : static unsigned print_faults = 0;
     103              : static unsigned print_overflows = 0;
     104              : static unsigned print_unprintables = 0;
     105              : 
     106        12729 : unsigned wok_print_fault_count(void) { return print_faults; }
     107         5047 : unsigned wok_print_overflow_count(void) { return print_overflows; }
     108         5334 : unsigned wok_print_unprintable_count(void) { return print_unprintables; }
     109        12729 : void wok_print_reports_reset(void) {
     110        12729 :   print_faults = 0;
     111        12729 :   print_overflows = 0;
     112        12729 :   print_unprintables = 0;
     113        12729 : }
     114              : 
     115              : // A break that would change the tree rather than the text. Counted in every
     116              : // build, because a caller about to overwrite a source file has to be able to
     117              : // ask whether this happened.
     118            0 : static void fault(Pr *p, const char *what) {
     119            0 :   if (p->count_only) return;
     120            0 :   print_faults++;
     121            0 :   if (p->quiet) return;
     122            0 :   (void)fprintf(stderr, "wok_print: INTERNAL: %s:%d: %s\n",
     123            0 :                 p->path ? p->path : "<input>", p->line, what);
     124              : }
     125              : 
     126      1560003 : static void sink_reserve(Pr *p, usize extra) {
     127      1560003 :   if (p->len + extra <= p->cap) return;
     128        23586 :   usize new_cap = p->cap ? p->cap * 2 : 256;
     129        23586 :   while (new_cap < p->len + extra) new_cap *= 2;
     130        23586 :   char *grown = WOK_NEW_N(p->arena, char, new_cap);
     131        23586 :   if (p->len) memcpy(grown, p->buf, p->len);
     132        23586 :   p->buf = grown;
     133        23586 :   p->cap = new_cap;
     134              : }
     135              : 
     136              : // THE LEAD TEST, applied to the bytes the filler is about to write. It answers
     137              : // the same question wok_token_is_continuation_lead answers about a scanned
     138              : // token, for the far smaller set of leads a fill break can produce; test_fill
     139              : // pins the two against each other for every one of them, which is what keeps
     140              : // this from becoming a second, drifting opinion about the lexis.
     141              : //
     142              : // The symbol charset itself is NOT restated here -- wok_token.c owns it, and a
     143              : // second copy is precisely how `-->` stops being a comment in one of them.
     144         6956 : static bool lead_is_continuation(const char *s, usize n) {
     145         6956 :   if (n == 0) return false;
     146         6956 :   unsigned char c0 = (unsigned char)s[0];
     147              :   // Every symbol run scans as an operator, and every operator is a lead. A
     148              :   // run of two or more dashes is a COMMENT, whose line produces no token at
     149              :   // all (L1), so it is safe on the same side of the answer.
     150         6956 :   if (wok_test_is_sym(c0)) return true;
     151         3602 :   switch (c0) {
     152              :     case ',':
     153              :     case '.':
     154              :     case '`':
     155              :     case ')':
     156              :     case ']':
     157              :     case '}':
     158              :       return true;
     159              :     default:
     160              :       break;
     161              :   }
     162              :   // The keywords that can never begin a block item.
     163              :   static const char *const words[] = {"with", "in",    "then", "else",
     164              :                                       "where", "of",   "as"};
     165          528 :   for (usize k = 0; k < sizeof words / sizeof *words; k++) {
     166          528 :     usize wl = strlen(words[k]);
     167          528 :     if (n < wl || memcmp(s, words[k], wl) != 0) continue;
     168              :     // A prefix is not the word: `within` begins an item.
     169          528 :     if (n == wl) return true;
     170          528 :     unsigned char after = (unsigned char)s[wl];
     171          528 :     if (!wok_test_is_ident_cont(after)) return true;
     172              :   }
     173              :   return false;
     174              : }
     175              : 
     176              : // The construct an over-width line is blamed on: the innermost node being
     177              : // written when the line crossed the width, which for an unbreakable run names
     178              : // the long application, literal or identifier directly.
     179          918 : static void blame(Pr *p) {
     180          918 :   p->over_comment = p->in_comment;
     181          918 :   p->over_tag = p->tn > 0 ? p->tstack[p->tn - 1] : (u16)W_File;
     182          918 : }
     183              : 
     184              : // Property 2: an overflow is STATED, so a long line reads as a limitation the
     185              : // printer names rather than as a formatter bug. It is never an error -- there
     186              : // is no other text this tree could have.
     187          918 : static void report_overflow(Pr *p) {
     188          918 :   print_overflows++;
     189          918 :   if (p->quiet) return;
     190          918 :   const char *where = p->path ? p->path : "<input>";
     191          918 :   if (p->over_comment) {
     192           72 :     (void)fprintf(stderr,
     193              :                   "wok_print: %s:%d: %zu columns over the %d-column width: a "
     194              :                   "comment, which a formatter may move but not rewrite\n",
     195              :                   where, p->line, p->cols, WOK_PRINT_WIDTH);
     196           72 :     return;
     197              :   }
     198          846 :   (void)fprintf(stderr,
     199              :                 "wok_print: %s:%d: %zu columns over the %d-column width: an "
     200              :                 "unbreakable run in %s -- juxtaposition, a literal and a name "
     201              :                 "own no separator to break at\n",
     202              :                 where, p->line, p->cols, WOK_PRINT_WIDTH,
     203          846 :                 wok_node_desc[p->over_tag].tag);
     204              : }
     205              : 
     206      2192922 : static void put_n(Pr *p, const char *s, usize n) {
     207              :   // The first bytes after a fill break must be a continuation lead, or the
     208              :   // new line is a new ITEM and the tree just changed. Checked on the bytes
     209              :   // themselves rather than on what the caller meant to write.
     210      2192922 :   if (p->check_lead && n != 0) {
     211         6956 :     p->check_lead = false;
     212         6956 :     if (!lead_is_continuation(s, n))
     213            0 :       fault(p, "a filled line does not begin with a continuation lead");
     214              :   }
     215              :   // Columns are counted in codepoints rather than bytes so a non-ASCII
     216              :   // literal in a pattern does not skew a block's alignment.
     217      7260221 :   for (usize i = 0; i < n; i++) {
     218      5067299 :     unsigned char ch = (unsigned char)s[i];
     219      5067299 :     if (ch == '\n') {
     220       151924 :       p->nl_seen = true;
     221       151924 :       if (!p->count_only) {
     222       151647 :         if (p->cols > WOK_PRINT_WIDTH) report_overflow(p);
     223       151647 :         p->line++;
     224       151647 :         p->line_over = false;
     225              :       }
     226       151924 :       p->cols = 0;
     227      4915375 :     } else if ((ch & 0xC0u) != 0x80u) {
     228      4915375 :       p->cols++;
     229      4915375 :       if (!p->count_only && !p->line_over && p->cols > WOK_PRINT_WIDTH) {
     230          918 :         p->line_over = true;
     231          918 :         blame(p);
     232              :       }
     233              :     }
     234              :   }
     235      2192922 :   if (p->count_only) return;
     236      1542040 :   if (p->to_file) {
     237            0 :     (void)fwrite(s, 1, n, p->file);
     238            0 :     return;
     239              :   }
     240      1542040 :   sink_reserve(p, n);
     241      1542040 :   memcpy(p->buf + p->len, s, n);
     242      1542040 :   p->len += n;
     243              : }
     244              : 
     245       774585 : static void put_c(Pr *p, char c) { put_n(p, &c, 1); }
     246              : 
     247       621186 : static void put_z(Pr *p, const char *s) { put_n(p, s, strlen(s)); }
     248              : 
     249       534400 : static void put_span(Pr *p, WokSpan s) { put_n(p, p->src + s.off, s.len); }
     250              : 
     251        25543 : static void put_uint(Pr *p, u64 v) {
     252        25543 :   char buf[32];
     253        25543 :   int len = snprintf(buf, sizeof buf, "%" PRIu64, v);
     254        25543 :   put_n(p, buf, (usize)(len < 0 ? 0 : len));
     255        25543 : }
     256              : 
     257         8107 : static void put_spaces(Pr *p, usize n) {
     258       177923 :   for (usize i = 0; i < n; i++) put_c(p, ' ');
     259         8107 : }
     260              : 
     261              : // What a new line CLAIMS to be. The layout filter reads that claim off the
     262              : // line's first token, so stating it here is what makes it checkable.
     263              : typedef enum {
     264              :   LN_ITEM,     // a block item: its column must be its block's own column
     265              :   LN_LEAD,     // a hanging continuation lead (`in`, `then`, `else`, `where`)
     266              :   LN_COMMENT,  // a comment-only line, which produces no token at all (L1)
     267              : } LineKind;
     268              : 
     269              : // The innermost open block's column, or 0 for the top level, whose items sit
     270              : // at column 0 and which no p_seq_block pushes. SIZE_MAX once nesting has
     271              : // outrun the tracked depth: a tree that deep cannot have come from a parse
     272              : // (the layout filter caps block nesting), and switching the check off is
     273              : // better than reporting a violation the printer did not commit.
     274        82876 : static usize block_col(const Pr *p) {
     275        82876 :   if (p->nblk > WOK_PRINT_BLOCK_MAX) return SIZE_MAX;
     276        82876 :   return p->nblk > 0 ? p->blk[p->nblk - 1] : 0;
     277              : }
     278              : 
     279              : // A line break plus `ind` levels of indentation. With nl_fill below, the ONLY
     280              : // place a newline is written, so "never inside brackets" and "an item line
     281              : // sits at its block's column" are both properties of these two functions.
     282        75876 : static void nl(Pr *p, u32 ind, LineKind kind) {
     283              :   // THE SAFETY PROPERTY, first half. An item line at any column but its
     284              :   // block's own either opens a block the parser does not expect or starts an
     285              :   // item in the wrong one; a continuation lead makes no such claim (L5) and
     286              :   // is deliberately allowed to sit shallower, which is how a hanging `in`
     287              :   // closes the binding's block.
     288        75876 :   usize bc = block_col(p);
     289        75876 :   if (kind == LN_ITEM && bc != SIZE_MAX && (usize)ind * 2 != bc)
     290            0 :     fault(p, "a block item is not at its block's column");
     291        75876 :   put_c(p, '\n');
     292       247182 :   for (u32 i = 0; i < ind; i++) put_n(p, "  ", 2);
     293        75876 : }
     294              : 
     295              : // A FILL break: a new line at absolute column `col`, whose first token the
     296              : // caller writes next. `in_bracket` says layout is suspended here (L2), where
     297              : // no column can mean anything; everywhere else the new line must be STRICTLY
     298              : // DEEPER than the enclosing block, since a continuation lead at or above that
     299              : // column emits DEDENTs (L4) and closes the block early.
     300         6956 : static void nl_fill(Pr *p, usize col, bool in_bracket) {
     301         6956 :   usize bc = block_col(p);
     302         6956 :   if (!in_bracket && bc != SIZE_MAX && col <= bc)
     303            0 :     fault(p, "a filled line is not deeper than its block");
     304         6956 :   put_c(p, '\n');
     305         6956 :   put_spaces(p, col);
     306         6956 :   p->check_lead = true;  // the next bytes written are checked, in put_n
     307         6956 : }
     308              : 
     309         4452 : static bool is_trailing_blank(char c) {
     310         4452 :   return c == ' ' || c == '\t' || c == '\r';
     311              : }
     312              : 
     313              : // A node the printer cannot render in canonical form. Reported rather than
     314              : // silently mangled, and the text that follows is expected to fail to
     315              : // re-parse -- exactly like the violation it is reporting.
     316            0 : static void unprintable(Pr *p, const char *what) {
     317              :   // Counted BEFORE the quiet check and skipped on a measuring pass, exactly as
     318              :   // `fault` is: every node is rendered at least twice, once into a width
     319              :   // counter and once for real, so counting on the measuring pass would double
     320              :   // every answer.
     321            0 :   if (p->count_only) return;
     322            0 :   print_unprintables++;
     323            0 :   if (p->quiet) return;
     324            0 :   (void)fprintf(stderr, "wok_print: %s cannot be written in canonical form\n",
     325              :                 what);
     326              : }
     327              : 
     328              : // ----------------------------------------------------------------- trivia
     329              : //
     330              : // The comments, put back. wok_trivia decided WHICH item each one belongs to;
     331              : // this decides only how it is written, and the whole of that is: a leading
     332              : // comment occupies the item's own line at the item's own indentation, and a
     333              : // trailing one follows the item after two spaces.
     334              : //
     335              : // A block comment is emitted VERBATIM, interior line breaks and all. A
     336              : // formatter may move a comment; it may not rewrite one.
     337              : 
     338       138315 : static const WokTriviaEntry *trivia_of(const Pr *p, const WokNode *n) {
     339       138315 :   return wok_trivia_lookup(p->tv, n);
     340              : }
     341              : 
     342         4452 : static void put_comment(Pr *p, u32 index) {
     343         4452 :   const WokComment *c = &p->tv->comment[index];
     344              :   // Blanks at the end of a comment are not part of what the author wrote, and
     345              :   // leaving them would put trailing whitespace on the line.
     346         4452 :   usize len = c->len;
     347         4452 :   while (len != 0 && is_trailing_blank(p->src[c->off + len - 1])) len--;
     348         4452 :   p->in_comment = true;  // an over-width comment is not an unbreakable RUN
     349         4452 :   put_n(p, p->src + c->off, len);
     350         4452 :   p->in_comment = false;
     351         4452 : }
     352              : 
     353              : // The paragraph break and the leading comments of one block item, each on its
     354              : // own line at `ind`. The item's own line break is the caller's.
     355        59337 : static void p_lead(Pr *p, const WokTriviaEntry *e, u32 ind, bool blank_ok) {
     356        59337 :   if (e == nullptr) return;
     357           79 :   if (blank_ok && e->blank_before != 0) put_c(p, '\n');
     358          130 :   for (u32 k = 0; k < e->lead_n; k++) {
     359           51 :     nl(p, ind, LN_COMMENT);
     360           51 :     put_comment(p, e->lead_first + k);
     361              :   }
     362              : }
     363              : 
     364              : // The comment written after an item, on the item's own line. A node whose
     365              : // trailing range belongs to a block it CLOSES is skipped here and written by
     366              : // p_block_close instead; writing both would print the comment twice.
     367       102013 : static void p_trail(Pr *p, const WokNode *n, const WokTriviaEntry *e) {
     368       102013 :   if (e == nullptr || wok_trivia_trail_closes_block(n)) return;
     369         1146 :   for (u32 k = 0; k < e->trail_n; k++) {
     370           34 :     put_z(p, "  ");
     371           34 :     put_comment(p, e->trail_first + k);
     372              :   }
     373              : }
     374              : 
     375              : // Rule 3: the comments that followed a block's last item, written at that
     376              : // block's own indentation.
     377        34406 : static void p_block_close(Pr *p, const WokNode *owner, u32 ind) {
     378        34406 :   if (owner == nullptr) return;
     379        18339 :   const WokTriviaEntry *e = trivia_of(p, owner);
     380        18339 :   if (e == nullptr) return;
     381           84 :   for (u32 k = 0; k < e->trail_n; k++) {
     382           18 :     nl(p, ind, LN_COMMENT);
     383           18 :     put_comment(p, e->trail_first + k);
     384              :   }
     385              : }
     386              : 
     387              : // ------------------------------------------------------------- precedence
     388              : //
     389              : // One ladder per family. The numbers of two families are never compared,
     390              : // because a node is only ever asked for the level its own family uses.
     391              : 
     392              : enum { TP_TYPE = 0, TP_ARROW = 1, TP_APP = 2, TP_ATOM = 3 };
     393              : enum { PP_PAT = 0, PP_APP = 1, PP_ATOM = 2 };
     394              : // EP_NEG sits BETWEEN the chain and the application, and it is not cosmetic.
     395              : // parse_app has two arms -- `- <the whole application to the right>`, and a
     396              : // juxtaposition of atoms -- and they share a rung without being
     397              : // interchangeable: a leading `-` SWALLOWS the rest of the rung, so negation is
     398              : // legal only at the HEAD of an application spine, never as the function of
     399              : // one. On one rung the printer wrote `E_App(E_Neg a, b)` as `-a b`, which
     400              : // reads back as `-(a b)`; the split is what asks for the bracket, and it also
     401              : // subsumes the hand-written case that used to keep `- -x` from lexing as a
     402              : // comment opener.
     403              : enum { EP_EXPR = 0, EP_CHAIN = 1, EP_NEG = 2, EP_APP = 3, EP_ATOM = 4 };
     404              : 
     405              : // Nodes that occupy no position where re-association is possible. It must be
     406              : // at least the top rung of EVERY ladder above, so that a node which cannot
     407              : // re-associate is never bracketed wherever it is placed. Inserting a rung and
     408              : // forgetting to raise this would put brackets around an N_Name in a module
     409              : // path or an H_ChainOp -- where a bracket is SYNTAX, not grouping -- so the
     410              : // invariant is asserted rather than described.
     411              : enum { PREC_FIXED = 4 };
     412              : // The casts carry weight: PREC_FIXED and the *_ATOM rungs are enumerators of
     413              : // DIFFERENT anonymous enums, and GCC's -Wenum-compare rejects comparing two
     414              : // unrelated enum types. Comparing their integer values is precisely what is
     415              : // meant here, so say so.
     416              : static_assert((int)PREC_FIXED >= (int)TP_ATOM &&
     417              :                   (int)PREC_FIXED >= (int)PP_ATOM &&
     418              :                   (int)PREC_FIXED >= (int)EP_ATOM,
     419              :               "PREC_FIXED must top every ladder; see the note above");
     420              : 
     421              : typedef struct {
     422              :   u32 prec;    // the level this position demands
     423              :   u32 ind;     // indentation of the line this node's head sits on
     424              :   bool flat;   // inside brackets: no BLOCK may be opened here
     425              :   bool lead;   // this node writes the FIRST token of its line
     426              :   bool brk;    // this node must break EVERY separator it owns
     427              :   usize pad;  // arrow alignment target, in columns; 0 for none
     428              :   // Columns that will CERTAINLY follow this node on its line: what the
     429              :   // unbreakable construct around it still has to write before the next place
     430              :   // a break is even possible. "Fits in the columns remaining" means remaining
     431              :   // after that, or a group measures itself, fits, and is then pushed over the
     432              :   // width by the juxtaposition it sits inside.
     433              :   usize rest;
     434              :   // The column a wrapped line inside this region takes, or 0 to derive it
     435              :   // from `ind`. Set by a bracketed group for its own contents, so a chain
     436              :   // wrapped inside a wrapped list lands under that list's items rather than
     437              :   // under the enclosing ITEM, where it would read as belonging to neither.
     438              :   usize wrap;
     439              : } Ctx;
     440              : 
     441      1088440 : static u32 node_prec(const WokNode *n) {
     442      1088440 :   switch ((WokTag)n->tag) {
     443              :     // Positions where a parenthesis would be meaningless or illegal.
     444              :     case N_Name:
     445              :     case N_ModPath:
     446              :     case D_Module:
     447              :     case D_Import:
     448              :     case D_Type:
     449              :     case D_Alias:
     450              :     case D_Effect:
     451              :     case D_Class:
     452              :     case D_Instance:
     453              :     case D_Foreign:
     454              :     case D_Fixity:
     455              :     case H_FixRel:
     456              :     case D_ExternType:
     457              :     case D_Sig:
     458              :     case D_Equation:
     459              :     case D_Error:
     460              :     case H_TyParam:
     461              :     case H_ConDef:
     462              :     case H_FieldType:
     463              :     case H_OpSig:
     464              :     case H_ForeignMember:
     465              :     case H_SigName:
     466              :     case L_Prefix:
     467              :     case L_Infix:
     468              :     case H_RowEntry:
     469              :     case H_FieldPat:
     470              :     case H_ChainOp:
     471              :     case H_Bind:
     472              :     case H_UseBind:
     473              :     case H_Alt:
     474              :     case H_Clause:
     475              :     case H_Field:
     476              :     case S_Let:
     477              :     case S_Handle:
     478              :     case S_Use:
     479              :     case S_Discard:
     480              :     case W_File:
     481              :       return PREC_FIXED;
     482              : 
     483       278099 :     case T_Var:
     484              :     case T_Con:
     485              :     case T_List:
     486              :     case T_Tuple:
     487              :     case T_Unit:
     488              :     case T_RowArg:
     489       278099 :       return TP_ATOM;
     490        19185 :     case T_App:
     491              :     case T_Transfer:
     492        19185 :       return TP_APP;
     493        26777 :     case T_Fun:
     494        26777 :       return TP_ARROW;
     495        42712 :     case T_Qual:
     496              :     case T_With:
     497        42712 :       return TP_TYPE;
     498              : 
     499        93916 :     case P_Var:
     500              :     case P_Wild:
     501              :     case P_Int:
     502              :     case P_Str:
     503              :     case P_Char:
     504              :     case P_Tuple:
     505              :     case P_List:
     506              :     case P_Unit:
     507              :     case P_As:
     508              :     case P_Record:
     509        93916 :       return PP_ATOM;
     510              :     // A bare constructor IS an atompat; an applied one is not.
     511         3592 :     case P_Con:
     512         3592 :       return P_Con_args(n).n != 0 ? PP_APP : PP_ATOM;
     513         3448 :     case P_Cons:
     514         3448 :       return PP_PAT;
     515              : 
     516              :     case E_Var:
     517              :     case E_Con:
     518              :     case E_Int:
     519              :     case E_Str:
     520              :     case E_Char:
     521              :     case E_Unit:
     522              :     case E_OpRef:
     523              :     case E_Dot:
     524              :     case E_List:
     525              :     case E_Tuple:
     526              :     case E_Record:
     527              :     case E_Error:
     528              :       return EP_ATOM;
     529         5326 :     case E_App:
     530         5326 :       return EP_APP;
     531         2404 :     case E_Neg:
     532         2404 :       return EP_NEG;
     533         5649 :     case E_Chain:
     534         5649 :       return EP_CHAIN;
     535        20634 :     case E_Lambda:
     536              :     case E_LetIn:
     537              :     case E_HandleIn:
     538              :     case E_UseIn:
     539              :     case E_If:
     540              :     case E_Case:
     541              :     case E_Handler:
     542              :     case E_Assign:
     543              :     case E_Block:
     544        20634 :       return EP_EXPR;
     545              : 
     546              :     case WOK_TAG_COUNT:
     547              :       break;
     548              :   }
     549            0 :   WOK_UNREACHABLE();
     550              : }
     551              : 
     552              : // Does this node begin with `-`? There are TWO positions where that is misread,
     553              : // and only `-` can arise in either: prefix negation, and a negative integer
     554              : // pattern.
     555              : //
     556              : //   1. the first token of a block ITEM. The layout filter reads an operator
     557              : //      lead as a CONTINUATION, so the line would join the previous one instead
     558              : //      of starting an item. Bracketing it is the repair the corpus already
     559              : //      writes by hand (`(-1) -> 2`).
     560              : //   2. the first ARGUMENT of a prefix left-hand side. parse_lhs decides
     561              : //      prefix-vs-infix on the single token after the name, so `f -1 = 2` is
     562              : //      read as the infix equation `f - 1 = 2` -- a different program that
     563              : //      still parses. A binding does not need the bracket (parse_bind has no
     564              : //      such lookahead), but the printer cannot tell an equation's left-hand
     565              : //      side from a binding's, so it writes the safe one for both.
     566       178256 : static bool needs_lead_paren(const WokNode *n) {
     567       178256 :   if (n->tag == E_Neg) return true;
     568       179351 :   return n->tag == P_Int && P_Int_negative(n);
     569              : }
     570              : 
     571              : // True when writing `n` at indent i leaves the cursor inside an indented
     572              : // block, so a following continuation lead (`in`, `then`, `else`, `where`)
     573              : // must start a new line shallower than that block. Written as a loop rather
     574              : // than a recursion because it descends only the LAST position of a node, and
     575              : // the loop cannot outrun the C stack.
     576         7211 : static bool opens_block(const WokNode *n) {
     577         8012 :   for (int guard = 0; guard < WOK_PRINT_MAX_DEPTH; guard++) {
     578         8012 :     switch ((WokTag)n->tag) {
     579              :       case E_Block:
     580              :       case E_Case:
     581              :       case E_Handler:
     582              :         return true;
     583           58 :       case E_LetIn:
     584           58 :         n = E_LetIn_body(n);
     585           58 :         break;
     586          360 :       case E_HandleIn:
     587          360 :         n = E_HandleIn_body(n);
     588          360 :         break;
     589           65 :       case E_UseIn:
     590           65 :         n = E_UseIn_body(n);
     591           65 :         break;
     592          131 :       case E_Lambda:
     593          131 :         n = E_Lambda_body(n);
     594          131 :         break;
     595           88 :       case E_If:
     596           88 :         n = E_If_else_(n);
     597           88 :         break;
     598           99 :       case E_Assign:
     599           99 :         n = E_Assign_value(n);
     600           99 :         break;
     601              : 
     602         6934 :       case N_Name:
     603              :       case N_ModPath:
     604              :       case D_Module:
     605              :       case D_Import:
     606              :       case D_Type:
     607              :       case D_Alias:
     608              :       case D_Effect:
     609              :       case D_Class:
     610              :       case D_Instance:
     611              :       case D_Foreign:
     612              :       case D_Fixity:
     613              :       case H_FixRel:
     614              :       case D_ExternType:
     615              :       case D_Sig:
     616              :       case D_Equation:
     617              :       case D_Error:
     618              :       case H_TyParam:
     619              :       case H_ConDef:
     620              :       case H_FieldType:
     621              :       case H_OpSig:
     622              :       case H_ForeignMember:
     623              :       case H_SigName:
     624              :       case L_Prefix:
     625              :       case L_Infix:
     626              :       case T_Var:
     627              :       case T_Con:
     628              :       case T_App:
     629              :       case T_Fun:
     630              :       case T_Qual:
     631              :       case T_With:
     632              :       case T_List:
     633              :       case T_Tuple:
     634              :       case T_Unit:
     635              :       case T_RowArg:
     636              :       case T_Transfer:
     637              :       case H_RowEntry:
     638              :       case P_Var:
     639              :       case P_Wild:
     640              :       case P_Int:
     641              :       case P_Str:
     642              :       case P_Char:
     643              :       case P_Con:
     644              :       case P_Cons:
     645              :       case P_Tuple:
     646              :       case P_List:
     647              :       case P_Unit:
     648              :       case P_As:
     649              :       case P_Record:
     650              :       case H_FieldPat:
     651              :       case E_Var:
     652              :       case E_Con:
     653              :       case E_Int:
     654              :       case E_Str:
     655              :       case E_Char:
     656              :       case E_Unit:
     657              :       case E_OpRef:
     658              :       case E_App:
     659              :       case E_Chain:
     660              :       case E_Dot:
     661              :       case E_Neg:
     662              :       case E_List:
     663              :       case E_Tuple:
     664              :       case E_Record:
     665              :       case E_Error:
     666              :       case S_Let:
     667              :       case S_Handle:
     668              :       case S_Use:
     669              :       case S_Discard:
     670              :       case H_ChainOp:
     671              :       case H_Bind:
     672              :       case H_UseBind:
     673              :       case H_Alt:
     674              :       case H_Clause:
     675              :       case H_Field:
     676              :       case W_File:
     677              :       case WOK_TAG_COUNT:
     678         6934 :         return false;
     679              :     }
     680              :   }
     681              :   return false;
     682              : }
     683              : 
     684              : // --------------------------------------------------------------- helpers
     685              : 
     686              : static void p_node(Pr *p, const WokNode *n, Ctx c);
     687              : 
     688              : // Every child starts with `brk` cleared: a group decides for itself whether it
     689              : // fits, so breaking an outer chain does not force an inner list to break.
     690              : //
     691              : // `rest` is cleared too, and that is exact rather than merely safe wherever a
     692              : // GROUP is the parent: if the group fit, everything inside it fit with room
     693              : // for the group's own `rest`, and if the group broke, this child is the last
     694              : // thing on its line. Only the UNBREAKABLE constructs -- juxtaposition, `.`,
     695              : // an arrow head -- have to pass a reserve on, and they say so with sub_rest.
     696       632395 : static Ctx sub(Ctx c, u32 prec) {
     697       632395 :   c.prec = prec;
     698       632395 :   c.lead = false;
     699       632395 :   c.brk = false;
     700       632395 :   c.pad = 0;
     701       632395 :   c.rest = 0;
     702       632395 :   return c;
     703              : }
     704              : 
     705              : // Keeps `lead`: this child writes the first token of the parent's line.
     706        89126 : static Ctx lead_sub(Ctx c, u32 prec) {
     707        89126 :   c.prec = prec;
     708        89126 :   c.brk = false;
     709        89126 :   c.pad = 0;
     710        89126 :   c.rest = 0;
     711        89126 :   return c;
     712              : }
     713              : 
     714        81699 : static Ctx sub_rest(Ctx c, u32 prec, usize extra) {
     715        81699 :   usize keep = c.rest + extra;
     716        81699 :   Ctx r = sub(c, prec);
     717        81699 :   r.rest = keep;
     718        81699 :   return r;
     719              : }
     720              : 
     721        37028 : static Ctx lead_rest(Ctx c, u32 prec, usize extra) {
     722        37028 :   usize keep = c.rest + extra;
     723        37028 :   Ctx r = lead_sub(c, prec);
     724        37028 :   r.rest = keep;
     725        37028 :   return r;
     726              : }
     727              : 
     728              : // Everything inside a bracket group. Layout is suspended there, so `flat` is
     729              : // what stops a BLOCK from being written where it would silently flatten.
     730       147115 : static Ctx bracket(Ctx c, u32 prec) {
     731       147115 :   c.prec = prec;
     732       147115 :   c.flat = true;
     733       147115 :   c.lead = false;
     734       147115 :   c.brk = false;
     735       147115 :   c.pad = 0;
     736       147115 :   c.rest = 0;
     737       147115 :   return c;
     738              : }
     739              : 
     740         2647 : static bool span_eq(const Pr *p, WokSpan a, WokSpan b) {
     741         2647 :   return a.len == b.len && memcmp(p->src + a.off, p->src + b.off, a.len) == 0;
     742              : }
     743              : 
     744              : // A signature and the equation it describes are ONE unit to a reader, and
     745              : // D23 makes the ordering law. A blank line between them, as "one blank line
     746              : // between top-level items" would give, reads as two unrelated declarations.
     747              : // Same for a run of imports under a module header.
     748        24714 : static bool decls_are_one_unit(const Pr *p, const WokNode *prev,
     749              :                                const WokNode *cur) {
     750        24714 :   if ((prev->tag == D_Module || prev->tag == D_Import) &&
     751         3137 :       (cur->tag == D_Module || cur->tag == D_Import))
     752              :     return true;
     753              :   // A run of `fixity` lines is one unit for the same reason a run of imports
     754              :   // is: each line is a fact about one operator, and the table they build is
     755              :   // read as a block. prelude/Base.wok writes twelve of them together.
     756        23989 :   if (prev->tag == D_Fixity && cur->tag == D_Fixity) return true;
     757        23533 :   if (prev->tag != D_Sig || cur->tag != D_Equation) return false;
     758         1657 :   const WokNode *lhs = D_Equation_lhs(cur);
     759         1657 :   if (lhs->tag != L_Prefix) return false;  // an infix LHS names no signature
     760         1609 :   WokSpan want = L_Prefix_name(lhs);
     761         1609 :   WokSeq names = D_Sig_names(prev);
     762         1749 :   for (u32 i = 0; i < names.n; i++)
     763         1635 :     if (span_eq(p, H_SigName_name(names.items[i]), want)) return true;
     764              :   return false;
     765              : }
     766              : 
     767              : // Backticks are a fact about the operator's SPELLING, not a stored bit: a
     768              : // name can only have been written between backticks, a symbol run only bare.
     769        34107 : static bool op_is_backticked(const Pr *p, WokSpan op) {
     770        34107 :   unsigned char c0 = op.len != 0 ? (unsigned char)p->src[op.off] : '+';
     771        34107 :   return (c0 >= 'a' && c0 <= 'z') || (c0 >= 'A' && c0 <= 'Z') || c0 == '_' ||
     772        25403 :          c0 >= 0x80;
     773              : }
     774              : 
     775        21224 : static void p_op(Pr *p, WokSpan op) {
     776        21224 :   if (op_is_backticked(p, op)) {
     777         5315 :     put_c(p, '`');
     778         5315 :     put_span(p, op);
     779         5315 :     put_c(p, '`');
     780              :   } else {
     781        15909 :     put_span(p, op);
     782              :   }
     783        21224 : }
     784              : 
     785              : // The columns p_op will write for `op`, backticks included.
     786        12883 : static usize op_cols(const Pr *p, WokSpan op) {
     787        12883 :   return op.len + (op_is_backticked(p, op) ? 2u : 0u);
     788              : }
     789              : 
     790       136143 : static void p_list(Pr *p, WokSeq s, Ctx c, const char *sep) {
     791       294952 :   for (u32 i = 0; i < s.n; i++) {
     792       158809 :     if (i != 0) put_z(p, sep);
     793       158809 :     p_node(p, s.items[i], c);
     794              :   }
     795       136143 : }
     796              : 
     797              : // Defined with the filler below; declared here because juxtaposition has to
     798              : // tell each argument how much of the line the arguments after it will take.
     799              : static usize flat_after(const Pr *p, const WokNode *n, Ctx c);
     800              : 
     801              : // Space-separated arguments, each at `prec`. Juxtaposition owns no separator,
     802              : // so every one of these arguments is UNCONDITIONALLY on this line: an
     803              : // argument's reserve is the flat width of the ones that follow it.
     804              : // `guard_first` marks the one caller whose FIRST argument sits in a position
     805              : // where a leading `-` is misread; see needs_lead_paren. Every other caller
     806              : // passes false, and says so at the call site rather than leaving it to be
     807              : // inferred from a default.
     808        52939 : static void p_args(Pr *p, WokSeq s, Ctx c, u32 prec, bool guard_first) {
     809        91301 :   for (u32 i = 0; i < s.n; i++) {
     810        38362 :     usize after = 0;
     811        49777 :     for (u32 j = i + 1; j < s.n; j++)
     812        11415 :       after += 1 + flat_after(p, s.items[j], sub(c, prec));
     813        38362 :     put_c(p, ' ');
     814        38362 :     Ctx ac = sub_rest(c, prec, after);
     815        38362 :     ac.lead = guard_first && i == 0;
     816        38362 :     p_node(p, s.items[i], ac);
     817              :   }
     818        52939 : }
     819              : 
     820              : // ----------------------------------------------------------- line filling
     821              : //
     822              : // The GROUPS: the nodes that own a separator the layout filter reads as a
     823              : // continuation lead, plus the bracketed lists, where layout is suspended
     824              : // outright. Every other node is unbreakable, so a long one overflows and says
     825              : // so. A group with one element owns no separator and so owns no break.
     826              : 
     827       637346 : static bool is_group(const WokNode *n) {
     828       637346 :   switch ((WokTag)n->tag) {
     829         2495 :     case E_Chain:
     830         2495 :       return E_Chain_ops(n).n != 0;
     831              :     case T_With:
     832              :     case T_Fun:
     833              :       return true;
     834         3148 :     case D_Type:
     835         3148 :       return D_Type_cons(n).n > 1;
     836         2552 :     case E_List:
     837         2552 :       return E_List_items(n).n > 1;
     838         2082 :     case E_Tuple:
     839         2082 :       return E_Tuple_items(n).n > 1;
     840         9132 :     case T_Tuple:
     841         9132 :       return T_Tuple_items(n).n > 1;
     842         2259 :     case P_List:
     843         2259 :       return P_List_items(n).n > 1;
     844         2721 :     case P_Tuple:
     845         2721 :       return P_Tuple_items(n).n > 1;
     846         2109 :     case E_Record:
     847         2736 :       return E_Record_fields(n).n + (E_Record_spread(n) != nullptr ? 1u : 0u) >
     848              :              1u;
     849         4241 :     case P_Record:
     850         6132 :       return P_Record_fields(n).n + (P_Record_is_open(n) ? 1u : 0u) > 1u;
     851         6550 :     case H_ConDef:
     852         8250 :       return H_ConDef_is_record(n) && H_ConDef_fields(n).n > 1;
     853              : 
     854       579907 :     case N_Name:
     855              :     case N_ModPath:
     856              :     case D_Module:
     857              :     case D_Import:
     858              :     case D_Alias:
     859              :     case D_Effect:
     860              :     case D_Class:
     861              :     case D_Instance:
     862              :     case D_Foreign:
     863              :     case D_ExternType:
     864              :     case D_Sig:
     865              :     case D_Fixity:
     866              :     case H_FixRel:
     867              :     case D_Equation:
     868              :     case D_Error:
     869              :     case H_TyParam:
     870              :     case H_FieldType:
     871              :     case H_OpSig:
     872              :     case H_ForeignMember:
     873              :     case H_SigName:
     874              :     case L_Prefix:
     875              :     case L_Infix:
     876              :     case T_Var:
     877              :     case T_Con:
     878              :     case T_App:
     879              :     case T_Qual:
     880              :     case T_List:
     881              :     case T_Unit:
     882              :     case T_RowArg:
     883              :     case T_Transfer:
     884              :     case H_RowEntry:
     885              :     case P_Var:
     886              :     case P_Wild:
     887              :     case P_Int:
     888              :     case P_Str:
     889              :     case P_Char:
     890              :     case P_Con:
     891              :     case P_Cons:
     892              :     case P_Unit:
     893              :     case P_As:
     894              :     case H_FieldPat:
     895              :     case E_Var:
     896              :     case E_Con:
     897              :     case E_Int:
     898              :     case E_Str:
     899              :     case E_Char:
     900              :     case E_Unit:
     901              :     case E_OpRef:
     902              :     case E_App:
     903              :     case E_Dot:
     904              :     case E_Neg:
     905              :     case E_Lambda:
     906              :     case E_LetIn:
     907              :     case E_HandleIn:
     908              :     case E_UseIn:
     909              :     case E_If:
     910              :     case E_Case:
     911              :     case E_Handler:
     912              :     case E_Assign:
     913              :     case E_Block:
     914              :     case E_Error:
     915              :     case S_Let:
     916              :     case S_Handle:
     917              :     case S_Use:
     918              :     case S_Discard:
     919              :     case H_ChainOp:
     920              :     case H_Bind:
     921              :     case H_UseBind:
     922              :     case H_Alt:
     923              :     case H_Clause:
     924              :     case H_Field:
     925              :     case W_File:
     926              :     case WOK_TAG_COUNT:
     927       579907 :       return false;
     928              :   }
     929            0 :   WOK_UNREACHABLE();
     930              : }
     931              : 
     932              : // The width `n` would occupy written on one line. Re-rendering into a counter
     933              : // is what the measure-only sink is for; `nofill` makes the measured pass take
     934              : // the flat arm of every group, so a measurement never recurses into another.
     935              : // A subtree that HAS to break a line -- a `case` written after `->`, say --
     936              : // has no flat width at all, and m.cols would report only its last line's,
     937              : // which is smaller than the truth. WOK_PRINT_UNBOUNDED says so instead.
     938              : enum { WOK_PRINT_UNBOUNDED = WOK_PRINT_WIDTH * 100 };
     939              : 
     940       105346 : static usize flat_cols(const Pr *p, const WokNode *n, Ctx c) {
     941       105346 :   Pr m = *p;
     942       105346 :   m.to_file = false;
     943       105346 :   m.count_only = true;
     944       105346 :   m.file = nullptr;
     945       105346 :   m.buf = nullptr;
     946       105346 :   m.len = 0;
     947       105346 :   m.cap = 0;
     948       105346 :   m.cols = 0;
     949       105346 :   m.quiet = true;
     950       105346 :   m.nofill = true;
     951       105346 :   m.nl_seen = false;
     952       105346 :   p_node(&m, n, c);
     953       105346 :   return m.nl_seen ? (usize)WOK_PRINT_UNBOUNDED : m.cols;
     954              : }
     955              : 
     956              : // The same width, or 0 during a measuring pass. A measuring pass never breaks,
     957              : // so it never needs to know what follows -- and asking would make measuring a
     958              : // juxtaposition cost one measurement per argument per level.
     959        82705 : static usize flat_after(const Pr *p, const WokNode *n, Ctx c) {
     960        82705 :   return p->nofill ? 0 : flat_cols(p, n, c);
     961              : }
     962              : 
     963              : // THE DECISION, and the whole of it: a group that does not fit in the columns
     964              : // remaining breaks at EVERY opportunity it owns. It depends only on the tree
     965              : // and on the column reached so far -- never on the text already emitted --
     966              : // which is what leaves the fixed point a corollary of tree identity rather
     967              : // than a separate risk.
     968      1088440 : static bool decide_break(const Pr *p, const WokNode *n, Ctx c) {
     969      1088440 :   if (p->nofill) return false;
     970       637346 :   if (!is_group(n)) return false;
     971        43718 :   return p->cols + flat_cols(p, n, c) + c.rest > WOK_PRINT_WIDTH;
     972              : }
     973              : 
     974              : // True when p_body will write `body` on the current line rather than as an
     975              : // indented block of its own.
     976        69651 : static bool body_is_inline(const WokNode *body, bool flat) {
     977        69651 :   return !(body->tag == E_Block && !flat);
     978              : }
     979              : 
     980              : // The columns p_body will add to the CURRENT line: the space and the body
     981              : // itself, or nothing when the body is a block of its own.
     982        14587 : static usize body_cols(const Pr *p, const WokNode *body, Ctx c) {
     983        14587 :   if (!body_is_inline(body, c.flat)) return 0;
     984        14336 :   usize w = flat_after(p, body, sub(c, EP_EXPR));
     985              :   // A body that opens a block of its own reserves nothing measurable: only
     986              :   // its FIRST line shares this one, and a reserve is a claim about this line.
     987              :   // Under-reserving can only leave a line over the width, which is reported;
     988              :   // over-reserving would wrap a head for an overflow no wrap can fix.
     989        14336 :   return w >= (usize)WOK_PRINT_UNBOUNDED ? 0 : 1 + w;
     990              : }
     991              : 
     992              : // Where a wrapped line starts. A capability row aligns under its own `with`
     993              : // (decision 1); everything else takes the item's indent plus one level, which
     994              : // is local and therefore stable under a rename.
     995         2629 : static usize cont_col(Ctx c) {
     996         2629 :   return c.wrap != 0 ? c.wrap : (usize)c.ind * 2 + 2;
     997              : }
     998              : 
     999              : // A bracketed, comma-separated group. The separator LEADS its line, so every
    1000              : // wrapped line begins with a continuation lead -- which is what keeps L7's
    1001              : // bracket-abandonment rule from firing on a wrapped element and what lets
    1002              : // wok_shape clamp the alignment instead of enforcing it.
    1003              : typedef struct {
    1004              :   usize col;  // the opener's column; separators and the closer align on it
    1005              :   bool brk;
    1006              : } Group;
    1007              : 
    1008        61450 : static Group group_open(Pr *p, const char *open, bool brk) {
    1009        61450 :   Group g = {.col = p->cols, .brk = brk};
    1010        61450 :   usize n = strlen(open);
    1011        61450 :   put_n(p, open, n);
    1012              :   // `[ a` rather than `[a`, so the first item clears the separator column.
    1013        61450 :   if (brk && open[n - 1] != ' ') put_c(p, ' ');
    1014        61450 :   return g;
    1015              : }
    1016              : 
    1017              : // The context for one item of `g`. Layout is suspended inside the brackets,
    1018              : // so what this fixes is legibility, not legality.
    1019       113666 : static Ctx group_item(Ctx c, u32 prec, Group g) {
    1020       113666 :   Ctx r = bracket(c, prec);
    1021       113666 :   r.wrap = g.col + 2;
    1022       113666 :   return r;
    1023              : }
    1024              : 
    1025        62008 : static void group_sep(Pr *p, Group g) {
    1026        62008 :   if (g.brk) nl_fill(p, g.col, true);
    1027        62008 :   put_z(p, ", ");
    1028        62008 : }
    1029              : 
    1030        61450 : static void group_close(Pr *p, Group g, const char *close) {
    1031        61450 :   if (!g.brk) {
    1032        60441 :     put_z(p, close);
    1033        60441 :     return;
    1034              :   }
    1035         1009 :   nl_fill(p, g.col, true);
    1036         1009 :   put_z(p, close[0] == ' ' ? close + 1 : close);
    1037              : }
    1038              : 
    1039              : // ------------------------------------------------------- arrow alignment
    1040              : 
    1041         4723 : static bool is_arrow_headed(const WokNode *n) {
    1042         4723 :   if (n->tag == H_Alt) return true;
    1043         3114 :   return n->tag == H_Clause && H_Clause_kind(n) != WOK_CLAUSE_VAR;
    1044              : }
    1045              : 
    1046              : // Everything left of the `->` of an arrow-headed item. Factored out because
    1047              : // a block must measure every head before it writes any of them.
    1048        16820 : static void p_arrow_head(Pr *p, const WokNode *n, Ctx c) {
    1049        16820 :   if (n->tag == H_Alt) {
    1050         8134 :     p_node(p, H_Alt_pat(n), lead_rest(c, PP_PAT, 0));
    1051         8134 :     return;
    1052              :   }
    1053         8686 :   switch (H_Clause_kind(n)) {
    1054         1899 :     case WOK_CLAUSE_CONTROL:
    1055         1899 :       put_span(p, H_Clause_name(n));
    1056              :       // `, k` still follows every pattern on this line, and it is two columns
    1057              :       // wider than the name, so a binder run wraps at the right place.
    1058         1899 :       c.rest += 2 + H_Clause_k(n).len;
    1059         1899 :       p_args(p, H_Clause_pats(n), c, PP_ATOM, false);
    1060         1899 :       put_z(p, ", ");
    1061         1899 :       put_span(p, H_Clause_k(n));
    1062         1899 :       return;
    1063         1725 :     case WOK_CLAUSE_ABORT:
    1064         1725 :       put_z(p, "abort ");
    1065         1725 :       put_span(p, H_Clause_name(n));
    1066         1725 :       p_args(p, H_Clause_pats(n), c, PP_ATOM, false);
    1067         1725 :       return;
    1068         2783 :     case WOK_CLAUSE_RETURN:
    1069         2783 :       put_z(p, "return");
    1070         2783 :       p_args(p, H_Clause_pats(n), c, PP_ATOM, false);
    1071         2783 :       return;
    1072         2279 :     case WOK_CLAUSE_PLAIN:
    1073         2279 :       put_span(p, H_Clause_name(n));
    1074         2279 :       p_args(p, H_Clause_pats(n), c, PP_ATOM, false);
    1075         2279 :       return;
    1076              :     case WOK_CLAUSE_VAR:
    1077              :       break;
    1078              :   }
    1079            0 :   WOK_UNREACHABLE();
    1080              : }
    1081              : 
    1082              : // What an arrow head still has to share its line with: the alignment padding
    1083              : // that follows it, the ` ->`, and the body when the body is written inline.
    1084              : //
    1085              : // Counting the body is the Wadler reading of "the columns remaining", and it
    1086              : // is what makes a wide pattern wrap when a two-character body tips the line
    1087              : // over. Its one wart: a head holding a breakable group, followed by a long
    1088              : // UNBREAKABLE body, wraps for an overflow the wrap cannot fix. That is
    1089              : // cosmetic, and the alternative -- ignoring the body -- leaves the far more
    1090              : // common case silently over the width.
    1091              : static usize head_cols(const Pr *p, const WokNode *n, Ctx c);
    1092              : 
    1093         6256 : static Ctx head_ctx(const Pr *p, const WokNode *n, Ctx c, const WokNode *body) {
    1094         6256 :   usize head = head_cols(p, n, c);
    1095         6256 :   usize slack = c.pad > head ? c.pad - head : 0;
    1096         6256 :   c.rest += slack + 3 + body_cols(p, body, c);
    1097         6256 :   return c;
    1098              : }
    1099              : 
    1100        10564 : static usize head_cols(const Pr *p, const WokNode *n, Ctx c) {
    1101        10564 :   Pr m = *p;
    1102        10564 :   m.to_file = false;
    1103        10564 :   m.count_only = true;
    1104        10564 :   m.file = nullptr;
    1105        10564 :   m.buf = nullptr;
    1106        10564 :   m.len = 0;
    1107        10564 :   m.cap = 0;
    1108        10564 :   m.cols = 0;
    1109        10564 :   m.quiet = true;
    1110        10564 :   m.nofill = true;  // a head never breaks a line, so measuring cannot either
    1111        10564 :   c.flat = true;
    1112        10564 :   c.pad = 0;
    1113        10564 :   p_arrow_head(&m, n, c);
    1114        10564 :   return m.cols;
    1115              : }
    1116              : 
    1117              : // ------------------------------------------------------------ block forms
    1118              : 
    1119              : // A seq block, ALWAYS in the indented form at `ind` -- unless `flat`, where
    1120              : // no indented form exists because layout is suspended inside brackets.
    1121              : // `align` marks the two blocks whose arrows line up (decision 6).
    1122              : //
    1123              : // `owner` is the node whose trailing trivia closes the block (rule 3), or
    1124              : // null for a `where`, which has no node of its own. Inside brackets no
    1125              : // comment can be attached at all -- wok_trivia marks those items FLAT and
    1126              : // gives them none -- so the flat arm has no trivia to write.
    1127        36803 : static void p_seq_block(Pr *p, const WokNode *owner, WokSeq seq, u32 ind,
    1128              :                         bool flat, bool align) {
    1129        36803 :   Ctx ic = {.prec = 0, .ind = ind, .flat = flat, .lead = true, .brk = false,
    1130              :             .pad = 0};
    1131              : 
    1132        36803 :   if (flat) {
    1133              :     // The parser can only have built a ONE-item block here, because L2
    1134              :     // suppresses every layout token between the brackets.
    1135         2397 :     if (seq.n != 1) unprintable(p, "a multi-item block inside brackets");
    1136         4794 :     for (u32 i = 0; i < seq.n; i++) {
    1137         2397 :       put_c(p, ' ');
    1138         2397 :       p_node(p, seq.items[i], (Ctx){
    1139              :                                   .prec = 0, .ind = ind, .flat = true,
    1140              :                                   .lead = false, .brk = false, .pad = 0});
    1141              :     }
    1142         2397 :     return;
    1143              :   }
    1144              : 
    1145              :   // This block's own column, which every item line of it must sit on and
    1146              :   // every filled line inside it must clear.
    1147        34406 :   if (p->nblk < WOK_PRINT_BLOCK_MAX) p->blk[p->nblk] = (u16)(ind * 2);
    1148        34406 :   p->nblk++;
    1149              : 
    1150        34406 :   if (align) {
    1151              :     usize widest = 0;
    1152         7823 :     for (u32 i = 0; i < seq.n; i++) {
    1153         4723 :       if (!is_arrow_headed(seq.items[i])) continue;
    1154         4308 :       usize w = head_cols(p, seq.items[i], ic);
    1155         4308 :       if (w > widest) widest = w;
    1156              :     }
    1157              :     // One long pattern must not push a whole block off the screen.
    1158         3100 :     if (widest <= WOK_PRINT_ALIGN_MAX) ic.pad = widest;
    1159              :   }
    1160              : 
    1161        93743 :   for (u32 i = 0; i < seq.n; i++) {
    1162        59337 :     const WokTriviaEntry *e = trivia_of(p, seq.items[i]);
    1163              :     // Nothing precedes the first item, so its paragraph break has nowhere to
    1164              :     // go; decision 4's "no blank line at the start of a block" still holds.
    1165        59337 :     p_lead(p, e, ind, i != 0);
    1166        59337 :     nl(p, ind, LN_ITEM);
    1167        59337 :     p_node(p, seq.items[i], ic);
    1168        59337 :     p_trail(p, seq.items[i], e);
    1169              :   }
    1170        34406 :   p_block_close(p, owner, ind);
    1171        34406 :   p->nblk--;
    1172              : }
    1173              : 
    1174              : // The right-hand side of `=` or `->`. A block body is the tree's own
    1175              : // statement, so it is written indented; anything else follows on the line.
    1176              : // `rest` is what the caller still writes after it on that line -- the ` in`
    1177              : // of a `let`, the ` else` of an `if`.
    1178        55064 : static void p_body(Pr *p, const WokNode *body, u32 ind, bool flat,
    1179              :                    usize rest) {
    1180        55064 :   if (!body_is_inline(body, flat)) {
    1181         1601 :     p_seq_block(p, body, E_Block_stmts(body), ind + 1, false, false);
    1182         1601 :     return;
    1183              :   }
    1184        53463 :   put_c(p, ' ');
    1185        53463 :   p_node(p, body,
    1186        53463 :          (Ctx){.prec = EP_EXPR, .ind = ind, .flat = flat, .lead = false,
    1187              :                .brk = false, .pad = 0, .rest = rest});
    1188              : }
    1189              : 
    1190              : // `where` sits one level under its item, and its bindings one level under
    1191              : // that. That column is exactly the body block's own column, which is what
    1192              : // keeps `where` INSIDE the body's block region -- where parse_equation and
    1193              : // parse_alt look for it -- instead of dedenting past it.
    1194        30189 : static void p_wheres(Pr *p, WokSeq w, u32 ind, bool flat) {
    1195        30189 :   if (w.n == 0) return;
    1196        16067 :   if (flat) {
    1197            0 :     put_z(p, " where");
    1198            0 :     p_seq_block(p, nullptr, w, ind, true, false);
    1199            0 :     return;
    1200              :   }
    1201        16067 :   nl(p, ind + 1, LN_LEAD);
    1202        16067 :   put_z(p, "where");
    1203        16067 :   p_seq_block(p, nullptr, w, ind + 2, false, false);
    1204              : }
    1205              : 
    1206              : // An inline body that OPENS a block must be pushed one level deeper when a
    1207              : // `where` follows, so the `where` line closes that block rather than landing
    1208              : // level with it.
    1209        30189 : static u32 body_indent(const WokNode *body, WokSeq wheres, u32 ind) {
    1210        30189 :   if (wheres.n == 0 || body->tag == E_Block) return ind;
    1211        15426 :   return ind + 1;
    1212              : }
    1213              : 
    1214              : // ------------------------------------------------------------- the switch
    1215              : 
    1216              : // `if` needs its three parts written in one of two shapes; keeping it out of
    1217              : // the giant switch keeps that decision readable.
    1218              : static void p_if(Pr *p, const WokNode *n, Ctx c);
    1219              : 
    1220      1088440 : static void p_node_inner(Pr *p, const WokNode *n, Ctx c) {
    1221      1088440 :   switch ((WokTag)n->tag) {
    1222              :     // ------------------------------------------------------------- names
    1223        91640 :     case N_Name:
    1224        91640 :       put_span(p, N_Name_text(n));
    1225        91640 :       return;
    1226        84689 :     case N_ModPath:
    1227        84689 :       p_list(p, N_ModPath_parts(n), sub(c, PREC_FIXED), ".");
    1228        84689 :       return;
    1229              : 
    1230              :     // ------------------------------------------------------ declarations
    1231         2989 :     case D_Module:
    1232         2989 :       put_z(p, "module ");
    1233         2989 :       p_node(p, D_Module_path(n), sub(c, PREC_FIXED));
    1234         2989 :       return;
    1235         2636 :     case D_Import: {
    1236         2636 :       put_z(p, "import ");
    1237         2636 :       p_node(p, D_Import_path(n), sub(c, PREC_FIXED));
    1238         2636 :       WokSeq names = D_Import_names(n);
    1239         2636 :       if (names.n != 0) {
    1240         1442 :         put_z(p, " (");
    1241         1442 :         p_list(p, names, bracket(c, PREC_FIXED), ", ");
    1242         1442 :         put_c(p, ')');
    1243              :       }
    1244         2636 :       if (D_Import_alias(n) != nullptr) {
    1245         1535 :         put_z(p, " as ");
    1246         1535 :         p_node(p, D_Import_alias(n), sub(c, PREC_FIXED));
    1247              :       }
    1248         2636 :       return;
    1249              :     }
    1250         5380 :     case D_Type: {
    1251         5380 :       put_z(p, "type ");
    1252         5380 :       put_span(p, D_Type_name(n));
    1253         5380 :       p_args(p, D_Type_params(n), c, PREC_FIXED, false);
    1254         5380 :       put_z(p, " = ");
    1255              :       // The FIRST alternative stays on the `=` line: a break before it would
    1256              :       // put an item lead at the head of the next line. Every later one is led
    1257              :       // by its `|`, which is a continuation lead and so may lead a line.
    1258         5380 :       WokSeq cons = D_Type_cons(n);
    1259        17564 :       for (u32 i = 0; i < cons.n; i++) {
    1260        12184 :         if (i != 0) {
    1261         6804 :           if (c.brk) {
    1262         1780 :             nl_fill(p, cont_col(c), c.flat);
    1263         1780 :             put_z(p, "| ");
    1264              :           } else {
    1265         5024 :             put_z(p, " | ");
    1266              :           }
    1267              :         }
    1268        12184 :         p_node(p, cons.items[i], sub(c, PREC_FIXED));
    1269              :       }
    1270         5380 :       return;
    1271              :     }
    1272         2272 :     case D_Alias:
    1273         2272 :       put_z(p, "alias ");
    1274         2272 :       put_span(p, D_Alias_name(n));
    1275         2272 :       p_args(p, D_Alias_params(n), c, PREC_FIXED, false);
    1276         2272 :       put_z(p, " = ");
    1277         2272 :       p_node(p, D_Alias_body(n), sub(c, TP_TYPE));
    1278         2272 :       return;
    1279         3259 :     case D_Effect:
    1280         3259 :       put_z(p, "effect ");
    1281         3259 :       put_span(p, D_Effect_name(n));
    1282         3259 :       p_args(p, D_Effect_params(n), c, PREC_FIXED, false);
    1283         3259 :       p_seq_block(p, n, D_Effect_ops(n), c.ind + 1, c.flat, false);
    1284         3259 :       return;
    1285         3617 :     case D_Class:
    1286         3617 :       put_z(p, "class ");
    1287         3617 :       put_span(p, D_Class_name(n));
    1288         3617 :       p_args(p, D_Class_params(n), c, PREC_FIXED, false);
    1289         3617 :       p_seq_block(p, n, D_Class_body(n), c.ind + 1, c.flat, false);
    1290         3617 :       return;
    1291         3865 :     case D_Instance:
    1292         3865 :       put_z(p, "instance ");
    1293              :       // The head's context is parenthesised by the GRAMMAR, not by
    1294              :       // precedence: parse_instance_decl demands the brackets.
    1295         3865 :       if (D_Instance_ctx(n) != nullptr) {
    1296         2566 :         put_c(p, '(');
    1297         2566 :         p_node(p, D_Instance_ctx(n), bracket(c, TP_TYPE));
    1298         2566 :         put_z(p, ") => ");
    1299              :       }
    1300         3865 :       put_span(p, D_Instance_name(n));
    1301         3865 :       p_args(p, D_Instance_args(n), c, TP_ATOM, false);
    1302         3865 :       p_seq_block(p, n, D_Instance_body(n), c.ind + 1, c.flat, false);
    1303         3865 :       return;
    1304         2897 :     case D_Foreign:
    1305         2897 :       put_z(p, "foreign module ");
    1306         2897 :       put_span(p, D_Foreign_name(n));
    1307         2897 :       put_c(p, ' ');
    1308         2897 :       put_span(p, D_Foreign_lib(n));
    1309         2897 :       p_seq_block(p, n, D_Foreign_members(n), c.ind + 1, c.flat, false);
    1310         2897 :       return;
    1311         2768 :     case D_ExternType:
    1312         2768 :       put_z(p, "extern type ");
    1313         2768 :       put_span(p, D_ExternType_name(n));
    1314         2768 :       p_args(p, D_ExternType_params(n), c, PREC_FIXED, false);
    1315         2768 :       return;
    1316        21789 :     case D_Sig:
    1317        21789 :       if (D_Sig_is_extern(n)) put_z(p, "extern ");
    1318        21789 :       p_list(p, D_Sig_names(n), sub(c, PREC_FIXED), ", ");
    1319        21789 :       put_z(p, " : ");
    1320        21789 :       p_node(p, D_Sig_type(n), sub(c, TP_TYPE));
    1321        21789 :       return;
    1322              :     // `fixity + left tighter than *`. One line, never filled: the relations
    1323              :     // are a sentence, and breaking `tighter than` across lines would put a
    1324              :     // continuation lead where the reader expects the rest of a phrase.
    1325         5183 :     case D_Fixity: {
    1326         5183 :       put_z(p, "fixity ");
    1327         5183 :       put_span(p, D_Fixity_name(n));
    1328         8115 :       put_z(p, D_Fixity_assoc(n) == WOK_ASSOC_RIGHT ? " right" : " left");
    1329         5183 :       WokSeq rels = D_Fixity_rels(n);
    1330        10701 :       for (u32 i = 0; i < rels.n; i++) {
    1331         5518 :         put_c(p, ' ');
    1332         5518 :         p_node(p, rels.items[i], sub(c, PREC_FIXED));
    1333              :       }
    1334         5183 :       return;
    1335              :     }
    1336         5518 :     case H_FixRel:
    1337         8272 :       put_z(p, H_FixRel_sense(n) == WOK_FIXREL_LOOSER ? "looser than "
    1338              :                                                       : "tighter than ");
    1339         5518 :       put_span(p, H_FixRel_name(n));
    1340         5518 :       return;
    1341        27134 :     case D_Equation: {
    1342        27134 :       WokSeq wheres = D_Equation_wheres(n);
    1343        27134 :       const WokNode *body = D_Equation_body(n);
    1344        27134 :       p_node(p, D_Equation_lhs(n), lead_sub(c, PP_PAT));
    1345        27134 :       put_z(p, " =");
    1346        27134 :       p_body(p, body, body_indent(body, wheres, c.ind), c.flat, 0);
    1347        27134 :       p_wheres(p, wheres, c.ind, c.flat);
    1348        27134 :       return;
    1349              :     }
    1350            0 :     case D_Error:
    1351              :       // A damaged item has no canonical form; its source text is the only
    1352              :       // faithful rendering, and a file holding one is not fit to format.
    1353            0 :       unprintable(p, "a damaged declaration");
    1354            0 :       put_span(p, D_Error_text(n));
    1355            0 :       return;
    1356              : 
    1357              :     // ----------------------------------------------- declaration helpers
    1358        22040 :     case H_TyParam:
    1359        22040 :       if (H_TyParam_is_row(n)) {
    1360        11027 :         put_z(p, "(row ");
    1361        11027 :         put_span(p, H_TyParam_name(n));
    1362        11027 :         put_c(p, ')');
    1363              :       } else {
    1364        11013 :         put_span(p, H_TyParam_name(n));
    1365              :       }
    1366              :       return;
    1367        14601 :     case H_ConDef: {
    1368        14601 :       WokSpan name = H_ConDef_name(n);
    1369        14601 :       if (!wok_span_empty(name)) put_span(p, name);
    1370        14601 :       if (H_ConDef_is_record(n)) {
    1371         9992 :         WokSeq fields = H_ConDef_fields(n);
    1372         9992 :         if (!wok_span_empty(name)) put_c(p, ' ');
    1373         9992 :         if (fields.n == 0) {
    1374         1109 :           put_z(p, "{}");
    1375              :         } else {
    1376         8883 :           Group g = group_open(p, "{ ", c.brk);
    1377        27964 :           for (u32 i = 0; i < fields.n; i++) {
    1378        19081 :             if (i != 0) group_sep(p, g);
    1379        19081 :             p_node(p, fields.items[i], group_item(c, PREC_FIXED, g));
    1380              :           }
    1381         8883 :           group_close(p, g, " }");
    1382              :         }
    1383              :       } else {
    1384         4609 :         p_args(p, H_ConDef_args(n), c, TP_ATOM, false);
    1385              :       }
    1386        14601 :       return;
    1387              :     }
    1388        19081 :     case H_FieldType:
    1389        19081 :       put_span(p, H_FieldType_name(n));
    1390        19081 :       put_z(p, " : ");
    1391        19081 :       p_node(p, H_FieldType_type(n), sub(c, TP_TYPE));
    1392        19081 :       return;
    1393         6545 :     case H_OpSig:
    1394         6545 :       put_span(p, H_OpSig_name(n));
    1395         6545 :       put_z(p, " : ");
    1396         6545 :       p_node(p, H_OpSig_type(n), sub(c, TP_TYPE));
    1397         6545 :       return;
    1398         6085 :     case H_ForeignMember: {
    1399         6085 :       put_span(p, H_ForeignMember_name(n));
    1400         6085 :       WokSpan sym = H_ForeignMember_symbol(n);
    1401         6085 :       if (!wok_span_empty(sym)) {
    1402         3994 :         put_c(p, ' ');
    1403         3994 :         put_span(p, sym);
    1404              :       }
    1405         6085 :       put_z(p, " : ");
    1406         6085 :       p_node(p, H_ForeignMember_type(n), sub(c, TP_TYPE));
    1407         6085 :       return;
    1408              :     }
    1409        29977 :     case H_SigName:
    1410        29977 :       if (H_SigName_paren(n)) {
    1411         7052 :         put_c(p, '(');
    1412         7052 :         put_span(p, H_SigName_name(n));
    1413         7052 :         put_c(p, ')');
    1414              :       } else {
    1415        22925 :         put_span(p, H_SigName_name(n));
    1416              :       }
    1417              :       return;
    1418        14891 :     case L_Prefix:
    1419        14891 :       if (L_Prefix_paren(n)) {
    1420         3443 :         put_c(p, '(');
    1421         3443 :         put_span(p, L_Prefix_name(n));
    1422         3443 :         put_c(p, ')');
    1423              :       } else {
    1424        11448 :         put_span(p, L_Prefix_name(n));
    1425              :       }
    1426        14891 :       p_args(p, L_Prefix_args(n), c, PP_ATOM, !L_Prefix_paren(n));
    1427        14891 :       return;
    1428        12883 :     case L_Infix: {
    1429        12883 :       const WokNode *right = L_Infix_right(n);
    1430        12883 :       WokSpan op = L_Infix_op(n);
    1431        12883 :       p_node(p, L_Infix_left(n),
    1432              :              lead_rest(c, PP_ATOM,
    1433        12883 :                        2 + op_cols(p, op) +
    1434        12883 :                            flat_after(p, right, sub(c, PP_ATOM))));
    1435        12883 :       put_c(p, ' ');
    1436        12883 :       p_op(p, op);
    1437        12883 :       put_c(p, ' ');
    1438        12883 :       p_node(p, right, sub(c, PP_ATOM));
    1439        12883 :       return;
    1440              :     }
    1441              : 
    1442              :     // ------------------------------------------------------------- types
    1443        64355 :     case T_Var:
    1444        64355 :       put_span(p, T_Var_name(n));
    1445        64355 :       return;
    1446        67830 :     case T_Con:
    1447        67830 :       p_node(p, T_Con_path(n), sub(c, PREC_FIXED));
    1448        67830 :       return;
    1449        18529 :     case T_App: {
    1450        18529 :       const WokNode *arg = T_App_arg(n);
    1451        18529 :       p_node(p, T_App_fn(n), sub_rest(c, TP_APP,
    1452        18529 :                                       1 + flat_after(p, arg, sub(c, TP_ATOM))));
    1453        18529 :       put_c(p, ' ');
    1454        18529 :       p_node(p, arg, sub(c, TP_ATOM));
    1455        18529 :       return;
    1456              :     }
    1457        26777 :     case T_Fun: {
    1458        26777 :       if (!c.brk) {
    1459        26413 :         p_node(p, T_Fun_from(n), sub(c, TP_APP));
    1460        26413 :         put_z(p, " -> ");
    1461        26413 :         p_node(p, T_Fun_to(n), sub(c, TP_ARROW));
    1462        26413 :         return;
    1463              :       }
    1464              :       // The whole arrow SPINE is one group, so every `->` in it breaks --
    1465              :       // walked here rather than left to the nested T_Fun, which would measure
    1466              :       // itself against the width freed by the break and decide to stay flat.
    1467          364 :       const WokNode *cur = n;
    1468          364 :       p_node(p, T_Fun_from(cur), sub(c, TP_APP));
    1469          179 :       for (;;) {
    1470          543 :         const WokNode *to = T_Fun_to(cur);
    1471          543 :         nl_fill(p, cont_col(c), c.flat);
    1472          543 :         put_z(p, "-> ");
    1473          543 :         if (to->tag != T_Fun) {
    1474          364 :           p_node(p, to, sub(c, TP_ARROW));
    1475          364 :           return;
    1476              :         }
    1477          179 :         p_node(p, T_Fun_from(to), sub(c, TP_APP));
    1478          179 :         cur = to;
    1479              :       }
    1480              :     }
    1481        16768 :     case T_Qual: {
    1482        16768 :       const WokNode *body = T_Qual_body(n);
    1483        16768 :       p_node(p, T_Qual_ctx(n),
    1484        16768 :              sub_rest(c, TP_ARROW, 4 + flat_after(p, body, sub(c, TP_TYPE))));
    1485        16768 :       put_z(p, " => ");
    1486        16768 :       p_node(p, body, sub(c, TP_TYPE));
    1487        16768 :       return;
    1488              :     }
    1489        25944 :     case T_With: {
    1490              :       // Decision 1: a wrapped capability row ALIGNS under its own `with`, and
    1491              :       // the `+` entries align under each other. `with` is a fixed anchor --
    1492              :       // renaming a row entry does not move it -- so the reflow is bounded,
    1493              :       // and wok_shape clamps the indentation of a continuation line anyway.
    1494        25944 :       usize start = p->cols;  // the column the whole type begins on
    1495        25944 :       p_node(p, T_With_body(n), sub(c, TP_ARROW));
    1496        25944 :       WokSeq row = T_With_row(n);
    1497        25944 :       if (!c.brk) {
    1498        25416 :         put_z(p, " with ");
    1499        25416 :         p_list(p, row, sub(c, PREC_FIXED), " + ");
    1500        25416 :         return;
    1501              :       }
    1502              :       // The alignment guard, for the same reason decision 6 has one: one long
    1503              :       // NAME must not push a whole row off the screen. The `with` column is
    1504              :       // set by what precedes the type, so a long function name would carry
    1505              :       // every row entry right with it. When alignment does not leave the
    1506              :       // widest entry room, the row falls back to the fixed item indent.
    1507              :       usize widest = 0;
    1508         1867 :       for (u32 i = 0; i < row.n; i++) {
    1509         1339 :         usize w = flat_cols(p, row.items[i], sub(c, PREC_FIXED));
    1510         1339 :         if (w > widest) widest = w;
    1511              :       }
    1512              :       // `with ` is five columns, and every entry begins in that column.
    1513          528 :       usize wcol = start;
    1514          528 :       if (!c.flat && (wcol <= block_col(p) ||
    1515           44 :                       wcol + 5 + widest > WOK_PRINT_WIDTH))
    1516            0 :         wcol = cont_col(c);
    1517          528 :       nl_fill(p, wcol, c.flat);
    1518          528 :       put_z(p, "with ");
    1519         1867 :       for (u32 i = 0; i < row.n; i++) {
    1520         1339 :         if (i != 0) {
    1521              :           // `+ ` is two columns, and `with ` is five: three deeper than the
    1522              :           // `with` puts the entries in one column.
    1523          811 :           nl_fill(p, wcol + 3, c.flat);
    1524          811 :           put_z(p, "+ ");
    1525              :         }
    1526         1339 :         p_node(p, row.items[i], sub(c, PREC_FIXED));
    1527              :       }
    1528              :       return;
    1529              :     }
    1530        15248 :     case T_List:
    1531        15248 :       put_c(p, '[');
    1532        15248 :       p_node(p, T_List_elem(n), bracket(c, TP_TYPE));
    1533        15248 :       put_c(p, ']');
    1534        15248 :       return;
    1535        24010 :     case T_Tuple: {
    1536        24010 :       WokSeq items = T_Tuple_items(n);
    1537        24010 :       Group g = group_open(p, "(", c.brk);
    1538        76279 :       for (u32 i = 0; i < items.n; i++) {
    1539        52269 :         if (i != 0) group_sep(p, g);
    1540        52269 :         p_node(p, items.items[i], group_item(c, TP_TYPE, g));
    1541              :       }
    1542        24010 :       group_close(p, g, ")");
    1543        24010 :       return;
    1544              :     }
    1545        55360 :     case T_Unit:
    1546        55360 :       put_z(p, "()");
    1547        55360 :       return;
    1548        51296 :     case T_RowArg:
    1549        51296 :       put_z(p, "(row ");
    1550        51296 :       put_span(p, T_RowArg_name(n));
    1551        51296 :       put_c(p, ')');
    1552        51296 :       return;
    1553          656 :     case T_Transfer:
    1554          656 :       switch (T_Transfer_mode(n)) {
    1555          297 :         case WOK_TRANSFER_OWN:
    1556          297 :           put_z(p, "own ");
    1557          297 :           break;
    1558          211 :         case WOK_TRANSFER_LEND:
    1559          211 :           put_z(p, "lend ");
    1560          211 :           break;
    1561          148 :         case WOK_TRANSFER_COPY:
    1562          148 :           put_z(p, "copy ");
    1563          148 :           break;
    1564            0 :         default:
    1565            0 :           unprintable(p, "an unknown FFI transfer mode");
    1566            0 :           break;
    1567              :       }
    1568          656 :       p_node(p, T_Transfer_body(n), sub_rest(c, TP_APP, 0));
    1569          656 :       return;
    1570        38179 :     case H_RowEntry:
    1571        38179 :       switch (H_RowEntry_kind(n)) {
    1572         7021 :         case WOK_ROW_ROLE:
    1573         7021 :           put_c(p, '(');
    1574         7021 :           put_span(p, H_RowEntry_label(n));
    1575         7021 :           put_z(p, " : ");
    1576         7021 :           p_node(p, H_RowEntry_type(n), bracket(c, TP_APP));
    1577         7021 :           put_c(p, ')');
    1578         7021 :           return;
    1579         8368 :         case WOK_ROW_VAR:
    1580         8368 :           put_z(p, "eff ");
    1581         8368 :           put_span(p, H_RowEntry_label(n));
    1582         8368 :           return;
    1583        22790 :         case WOK_ROW_SLOT:
    1584              :           // parse_rowentry reads a SLOT at typeapp level, so anything
    1585              :           // looser has to be bracketed to survive the round trip.
    1586        22790 :           p_node(p, H_RowEntry_type(n), sub(c, TP_APP));
    1587        22790 :           return;
    1588            0 :         default:
    1589            0 :           unprintable(p, "an unknown row entry kind");
    1590            0 :           return;
    1591              :       }
    1592              : 
    1593              :     // ---------------------------------------------------------- patterns
    1594        15069 :     case P_Var:
    1595        15069 :       put_span(p, P_Var_name(n));
    1596        15069 :       return;
    1597        12804 :     case P_Wild:
    1598        12804 :       put_c(p, '_');
    1599        12804 :       return;
    1600        10385 :     case P_Int:
    1601        10385 :       if (P_Int_negative(n)) put_c(p, '-');
    1602        10385 :       put_uint(p, P_Int_value(n));
    1603        10385 :       return;
    1604         9854 :     case P_Str:
    1605         9854 :       put_span(p, P_Str_text(n));
    1606         9854 :       return;
    1607        10884 :     case P_Char:
    1608        10884 :       put_span(p, P_Char_text(n));
    1609        10884 :       return;
    1610         3592 :     case P_Con:
    1611         3592 :       p_node(p, P_Con_path(n), lead_sub(c, PREC_FIXED));
    1612         3592 :       p_args(p, P_Con_args(n), c, PP_ATOM, false);
    1613         3592 :       return;
    1614         3448 :     case P_Cons: {
    1615         3448 :       const WokNode *tail = P_Cons_tail(n);
    1616         3448 :       p_node(p, P_Cons_head(n),
    1617         3448 :              lead_rest(c, PP_APP, 4 + flat_after(p, tail, sub(c, PP_PAT))));
    1618         3448 :       put_z(p, " :: ");
    1619         3448 :       p_node(p, tail, sub(c, PP_PAT));
    1620         3448 :       return;
    1621              :     }
    1622         7578 :     case P_Tuple: {
    1623         7578 :       WokSeq items = P_Tuple_items(n);
    1624         7578 :       Group g = group_open(p, "(", c.brk);
    1625        23702 :       for (u32 i = 0; i < items.n; i++) {
    1626        16124 :         if (i != 0) group_sep(p, g);
    1627        16124 :         p_node(p, items.items[i], group_item(c, PP_PAT, g));
    1628              :       }
    1629         7578 :       group_close(p, g, ")");
    1630         7578 :       return;
    1631              :     }
    1632         3899 :     case P_List: {
    1633         3899 :       WokSeq items = P_List_items(n);
    1634         3899 :       Group g = group_open(p, "[", c.brk);
    1635         6002 :       for (u32 i = 0; i < items.n; i++) {
    1636         2103 :         if (i != 0) group_sep(p, g);
    1637         2103 :         p_node(p, items.items[i], group_item(c, PP_PAT, g));
    1638              :       }
    1639         3899 :       group_close(p, g, "]");
    1640         3899 :       return;
    1641              :     }
    1642        12257 :     case P_Unit:
    1643        12257 :       put_z(p, "()");
    1644        12257 :       return;
    1645         3544 :     case P_As:
    1646         3544 :       p_node(p, P_As_pat(n),
    1647         3544 :              lead_rest(c, PP_ATOM, 4 + P_As_name(n).len));
    1648         3544 :       put_z(p, " as ");
    1649         3544 :       put_span(p, P_As_name(n));
    1650         3544 :       return;
    1651         7642 :     case P_Record: {
    1652         7642 :       WokSeq fields = P_Record_fields(n);
    1653         7642 :       bool open = P_Record_is_open(n);
    1654         7642 :       p_node(p, P_Record_path(n), lead_sub(c, PREC_FIXED));
    1655         7642 :       put_c(p, ' ');
    1656         7642 :       if (!open && fields.n == 0) {
    1657         2609 :         put_z(p, "{}");
    1658         2609 :         return;
    1659              :       }
    1660         5033 :       Group g = group_open(p, "{ ", c.brk);
    1661         5033 :       bool first = true;
    1662         5033 :       if (open) {
    1663         4455 :         put_z(p, "..");
    1664         4455 :         WokSpan rest = P_Record_rest(n);
    1665         4455 :         if (!wok_span_empty(rest)) {
    1666         2942 :           put_c(p, ' ');
    1667         2942 :           put_span(p, rest);
    1668              :         }
    1669         4455 :         first = false;
    1670              :       }
    1671         7368 :       for (u32 i = 0; i < fields.n; i++) {
    1672         2335 :         if (!first) group_sep(p, g);
    1673         2335 :         first = false;
    1674         2335 :         p_node(p, fields.items[i], group_item(c, PREC_FIXED, g));
    1675              :       }
    1676         5033 :       group_close(p, g, " }");
    1677         5033 :       return;
    1678              :     }
    1679         2335 :     case H_FieldPat: {
    1680         2335 :       WokSpan name = H_FieldPat_name(n);
    1681         2335 :       const WokNode *pat = H_FieldPat_pat(n);
    1682              :       // `{ x }` and `{ x = x }` are one tree; the punned spelling is the
    1683              :       // canonical one.
    1684         2335 :       if (pat->tag == P_Var && span_eq(p, P_Var_name(pat), name)) {
    1685          117 :         put_span(p, name);
    1686          117 :         return;
    1687              :       }
    1688         2218 :       put_span(p, name);
    1689         2218 :       put_z(p, " = ");
    1690         2218 :       p_node(p, pat, bracket(c, PP_PAT));
    1691         2218 :       return;
    1692              :     }
    1693              : 
    1694              :     // ------------------------------------------------------- expressions
    1695        18745 :     case E_Var:
    1696        18745 :       put_span(p, E_Var_name(n));
    1697        18745 :       return;
    1698        15530 :     case E_Con:
    1699        15530 :       put_span(p, E_Con_name(n));
    1700        15530 :       return;
    1701        15158 :     case E_Int:
    1702        15158 :       put_uint(p, E_Int_value(n));
    1703        15158 :       return;
    1704        11766 :     case E_Str:
    1705        11766 :       put_span(p, E_Str_text(n));
    1706        11766 :       return;
    1707         9430 :     case E_Char:
    1708         9430 :       put_span(p, E_Char_text(n));
    1709         9430 :       return;
    1710        11131 :     case E_Unit:
    1711        11131 :       put_z(p, "()");
    1712        11131 :       return;
    1713        11981 :     case E_OpRef:
    1714        11981 :       put_c(p, '(');
    1715        11981 :       put_span(p, E_OpRef_name(n));
    1716        11981 :       put_c(p, ')');
    1717        11981 :       return;
    1718         5326 :     case E_App: {
    1719         5326 :       const WokNode *arg = E_App_arg(n);
    1720         5326 :       p_node(p, E_App_fn(n),
    1721         5326 :              lead_rest(c, EP_APP, 1 + flat_after(p, arg, sub(c, EP_ATOM))));
    1722         5326 :       put_c(p, ' ');
    1723         5326 :       p_node(p, arg, sub(c, EP_ATOM));
    1724         5326 :       return;
    1725              :     }
    1726         5649 :     case E_Chain: {
    1727              :       // EP_NEG, not EP_APP: `-a + b` is one chain whose head is a negation,
    1728              :       // and a spine head is exactly where negation is legal bare.
    1729         5649 :       p_node(p, E_Chain_head(n), lead_sub(c, EP_NEG));
    1730         5649 :       WokSeq ops = E_Chain_ops(n);
    1731        13990 :       for (u32 i = 0; i < ops.n; i++) {
    1732              :         // Every operand is separated by an OPERATOR, which is the one token
    1733              :         // class the layout filter reads as continuing the line.
    1734         8341 :         if (c.brk)
    1735          306 :           nl_fill(p, cont_col(c), c.flat);
    1736              :         else
    1737         8035 :           put_c(p, ' ');
    1738         8341 :         p_node(p, ops.items[i], sub(c, PREC_FIXED));
    1739              :       }
    1740         5649 :       return;
    1741              :     }
    1742         8341 :     case H_ChainOp:
    1743         8341 :       p_op(p, H_ChainOp_op(n));
    1744         8341 :       put_c(p, ' ');
    1745         8341 :       p_node(p, H_ChainOp_rhs(n), sub(c, EP_NEG));  // `a + -b`
    1746         8341 :       return;
    1747         3693 :     case E_Dot:
    1748         3693 :       p_node(p, E_Dot_recv(n), lead_rest(c, EP_ATOM, 1 + E_Dot_name(n).len));
    1749         3693 :       put_c(p, '.');
    1750         3693 :       put_span(p, E_Dot_name(n));
    1751         3693 :       return;
    1752         2404 :     case E_Neg:
    1753         2404 :       put_c(p, '-');
    1754              :       // Negation takes the whole application to its right, so its body is an
    1755              :       // application spine. A nested negation is looser than that and brackets
    1756              :       // itself, which is also what keeps `- -x` from lexing as the comment
    1757              :       // opener `--`; the ladder states it, so no case here has to.
    1758         2404 :       p_node(p, E_Neg_body(n), sub_rest(c, EP_APP, 0));
    1759         2404 :       return;
    1760         4316 :     case E_List: {
    1761         4316 :       WokSeq items = E_List_items(n);
    1762         4316 :       Group g = group_open(p, "[", c.brk);
    1763         8620 :       for (u32 i = 0; i < items.n; i++) {
    1764         4304 :         if (i != 0) group_sep(p, g);
    1765         4304 :         p_node(p, items.items[i], group_item(c, EP_EXPR, g));
    1766              :       }
    1767         4316 :       group_close(p, g, "]");
    1768         4316 :       return;
    1769              :     }
    1770         4647 :     case E_Tuple: {
    1771         4647 :       WokSeq items = E_Tuple_items(n);
    1772         4647 :       Group g = group_open(p, "(", c.brk);
    1773        14859 :       for (u32 i = 0; i < items.n; i++) {
    1774        10212 :         if (i != 0) group_sep(p, g);
    1775        10212 :         p_node(p, items.items[i], group_item(c, EP_EXPR, g));
    1776              :       }
    1777         4647 :       group_close(p, g, ")");
    1778         4647 :       return;
    1779              :     }
    1780         2205 :     case E_Lambda: {
    1781         2205 :       put_c(p, '\\');
    1782         2205 :       WokSeq params = E_Lambda_params(n);
    1783         2637 :       for (u32 i = 0; i < params.n; i++) {
    1784          432 :         if (i != 0) put_c(p, ' ');
    1785          432 :         p_node(p, params.items[i], sub(c, PP_ATOM));
    1786              :       }
    1787         2205 :       put_z(p, " ->");
    1788         2205 :       p_body(p, E_Lambda_body(n), c.ind, c.flat, c.rest);
    1789         2205 :       return;
    1790              :     }
    1791         1660 :     case E_LetIn: {
    1792         1660 :       const WokNode *bind = E_LetIn_bind(n);
    1793         1660 :       const WokNode *lbody = E_LetIn_body(n);
    1794         1660 :       bool hang = !c.flat && opens_block(H_Bind_body(bind));
    1795         1660 :       put_z(p, "let ");
    1796         3320 :       p_node(p, bind,
    1797           31 :              hang ? sub(c, PREC_FIXED)
    1798         1629 :                   : sub_rest(c, PREC_FIXED, 3 + body_cols(p, lbody, c)));
    1799              :       // `in` closes the binding's block, so it must start a line SHALLOWER
    1800              :       // than that block when there is one.
    1801         1660 :       if (hang) {
    1802           31 :         nl(p, c.ind, LN_LEAD);
    1803           31 :         put_z(p, "in");
    1804              :       } else {
    1805         1629 :         put_z(p, " in");
    1806              :       }
    1807         1660 :       p_body(p, lbody, c.ind, c.flat, c.rest);
    1808         1660 :       return;
    1809              :     }
    1810         2331 :     case E_HandleIn: {
    1811         2331 :       const WokNode *handler = E_HandleIn_handler(n);
    1812         2331 :       WokSpan label = E_HandleIn_label(n);
    1813         2331 :       put_z(p, "handle ");
    1814         2331 :       if (!wok_span_empty(label)) {
    1815         1357 :         put_span(p, label);
    1816         1357 :         put_z(p, " = ");
    1817              :       }
    1818         2331 :       p_node(p, handler, sub(c, EP_EXPR));
    1819         2331 :       if (!c.flat && opens_block(handler)) {
    1820           36 :         nl(p, c.ind, LN_LEAD);
    1821           36 :         put_z(p, "in");
    1822              :       } else {
    1823         2295 :         put_z(p, " in");
    1824              :       }
    1825         2331 :       p_body(p, E_HandleIn_body(n), c.ind, c.flat, c.rest);
    1826         2331 :       return;
    1827              :     }
    1828         2719 :     case E_UseIn:
    1829         2719 :       put_z(p, "use ");
    1830         2719 :       p_list(p, E_UseIn_binds(n), sub(c, PREC_FIXED), ", ");
    1831         2719 :       put_z(p, " in");
    1832         2719 :       p_body(p, E_UseIn_body(n), c.ind, c.flat, c.rest);
    1833         2719 :       return;
    1834         3477 :     case E_If:
    1835         3477 :       p_if(p, n, c);
    1836         3477 :       return;
    1837         2317 :     case E_Case: {
    1838         2317 :       const WokNode *scrut = E_Case_scrut(n);
    1839         2317 :       bool split = !c.flat && opens_block(scrut);
    1840         2317 :       put_z(p, "case ");
    1841              :       {
    1842         2317 :         Ctx sc = sub(c, EP_EXPR);
    1843         2317 :         if (split) sc.ind = c.ind + 1;
    1844         2317 :         p_node(p, scrut, sc);
    1845              :       }
    1846         2317 :       if (split) {
    1847           84 :         nl(p, c.ind + 1, LN_LEAD);
    1848           84 :         put_z(p, "of");
    1849              :       } else {
    1850         2233 :         put_z(p, " of");
    1851              :       }
    1852         2317 :       p_seq_block(p, n, E_Case_alts(n), c.ind + 1, c.flat, true);
    1853         2317 :       return;
    1854              :     }
    1855         3180 :     case E_Handler:
    1856         3180 :       put_z(p, "handler ");
    1857         3180 :       put_span(p, E_Handler_effect(n));
    1858         3180 :       p_seq_block(p, n, E_Handler_clauses(n), c.ind + 1, c.flat, true);
    1859         3180 :       return;
    1860         2745 :     case E_Assign:
    1861         2745 :       p_node(p, E_Assign_target(n), lead_sub(c, EP_CHAIN));
    1862         2745 :       put_z(p, " :=");
    1863         2745 :       p_body(p, E_Assign_value(n), c.ind, c.flat, c.rest);
    1864         2745 :       return;
    1865         3276 :     case E_Record: {
    1866         3276 :       WokSeq fields = E_Record_fields(n);
    1867         3276 :       const WokNode *spread = E_Record_spread(n);
    1868         3276 :       p_node(p, E_Record_path(n), lead_sub(c, EP_ATOM));
    1869         3276 :       put_c(p, ' ');
    1870         3276 :       if (spread == nullptr && fields.n == 0) {
    1871          192 :         put_z(p, "{}");
    1872          192 :         return;
    1873              :       }
    1874         3084 :       Group g = group_open(p, "{ ", c.brk);
    1875         3084 :       bool first = true;
    1876         3084 :       if (spread != nullptr) {
    1877         2208 :         put_z(p, ".. ");
    1878         2208 :         p_node(p, spread, group_item(c, EP_EXPR, g));
    1879         2208 :         first = false;
    1880              :       }
    1881         8114 :       for (u32 i = 0; i < fields.n; i++) {
    1882         5030 :         if (!first) group_sep(p, g);
    1883         5030 :         first = false;
    1884         5030 :         p_node(p, fields.items[i], group_item(c, PREC_FIXED, g));
    1885              :       }
    1886         3084 :       group_close(p, g, " }");
    1887         3084 :       return;
    1888              :     }
    1889            0 :     case E_Block:
    1890              :       // Reached only when a block sits somewhere p_body did not place it.
    1891            0 :       p_seq_block(p, n, E_Block_stmts(n), c.ind + 1, c.flat, false);
    1892            0 :       return;
    1893            0 :     case E_Error:
    1894            0 :       unprintable(p, "a damaged expression");
    1895            0 :       put_span(p, E_Error_text(n));
    1896            0 :       return;
    1897              : 
    1898              :     // -------------------------------------------------------- statements
    1899          400 :     case S_Let:
    1900          400 :       put_z(p, "let ");
    1901          400 :       p_node(p, S_Let_bind(n), sub(c, PREC_FIXED));
    1902          400 :       return;
    1903          408 :     case S_Handle:
    1904          408 :       put_z(p, "handle ");
    1905          408 :       put_span(p, S_Handle_label(n));
    1906          408 :       put_z(p, " = ");
    1907          408 :       p_node(p, S_Handle_handler(n), sub(c, EP_EXPR));
    1908          408 :       return;
    1909           88 :     case S_Use:
    1910           88 :       put_z(p, "use ");
    1911           88 :       p_list(p, S_Use_binds(n), sub(c, PREC_FIXED), ", ");
    1912           88 :       return;
    1913          136 :     case S_Discard:
    1914          136 :       put_z(p, "_ =");
    1915          136 :       p_body(p, S_Discard_body(n), c.ind, c.flat, c.rest);
    1916          136 :       return;
    1917              : 
    1918              :     // ------------------------------------------------ expression helpers
    1919         2060 :     case H_Bind:
    1920         2060 :       p_node(p, H_Bind_lhs(n), lead_sub(c, PP_PAT));
    1921         2060 :       put_z(p, " =");
    1922         2060 :       p_body(p, H_Bind_body(n), c.ind, c.flat, c.rest);
    1923         2060 :       return;
    1924         3226 :     case H_UseBind:
    1925         3226 :       put_span(p, H_UseBind_from(n));
    1926         3226 :       put_z(p, " as ");
    1927         3226 :       put_span(p, H_UseBind_to(n));
    1928         3226 :       return;
    1929         3055 :     case H_Alt: {
    1930         3055 :       WokSeq wheres = H_Alt_wheres(n);
    1931         3055 :       const WokNode *body = H_Alt_body(n);
    1932         3055 :       usize start = p->cols;
    1933         3055 :       p_arrow_head(p, n, head_ctx(p, n, c, body));
    1934         3055 :       if (c.pad > p->cols - start) put_spaces(p, c.pad - (p->cols - start));
    1935         3055 :       put_z(p, " ->");
    1936         3055 :       p_body(p, body, body_indent(body, wheres, c.ind), c.flat, 0);
    1937         3055 :       p_wheres(p, wheres, c.ind, c.flat);
    1938         3055 :       return;
    1939              :     }
    1940         4065 :     case H_Clause: {
    1941         4065 :       if (H_Clause_kind(n) == WOK_CLAUSE_VAR) {
    1942              :         // A baton is headed by `=`, so it joins no alignment group.
    1943          864 :         put_z(p, "var ");
    1944          864 :         put_span(p, H_Clause_name(n));
    1945          864 :         put_z(p, " =");
    1946          864 :         p_body(p, H_Clause_body(n), c.ind, c.flat, c.rest);
    1947          864 :         return;
    1948              :       }
    1949         3201 :       usize start = p->cols;
    1950         3201 :       p_arrow_head(p, n, head_ctx(p, n, c, H_Clause_body(n)));
    1951         3201 :       if (c.pad > p->cols - start) put_spaces(p, c.pad - (p->cols - start));
    1952         3201 :       put_z(p, " ->");
    1953         3201 :       p_body(p, H_Clause_body(n), c.ind, c.flat, c.rest);
    1954         3201 :       return;
    1955              :     }
    1956         5030 :     case H_Field: {
    1957         5030 :       WokSpan name = H_Field_name(n);
    1958         5030 :       const WokNode *value = H_Field_value(n);
    1959         5030 :       if (value->tag == E_Var && span_eq(p, E_Var_name(value), name)) {
    1960           76 :         put_span(p, name);
    1961           76 :         return;
    1962              :       }
    1963         4954 :       put_span(p, name);
    1964         4954 :       put_z(p, " = ");
    1965         4954 :       p_node(p, value, bracket(c, EP_EXPR));
    1966         4954 :       return;
    1967              :     }
    1968              : 
    1969              :     // -------------------------------------------------------------- file
    1970        17963 :     case W_File: {
    1971        17963 :       WokSeq decls = W_File_decls(n);
    1972        60639 :       for (u32 i = 0; i < decls.n; i++) {
    1973              :         // One blank line between top-level items, except where two of them
    1974              :         // are really one unit (a signature and its equation, a run of imports).
    1975              :         // Decision 4 owns the blank line here, so a declaration's recorded
    1976              :         // paragraph break is not consulted -- honouring both would print two.
    1977        42676 :         if (i != 0 && !decls_are_one_unit(p, decls.items[i - 1], decls.items[i]))
    1978        22038 :           put_c(p, '\n');
    1979        42676 :         const WokTriviaEntry *e = trivia_of(p, decls.items[i]);
    1980        42676 :         if (e != nullptr)
    1981         4993 :           for (u32 k = 0; k < e->lead_n; k++) {
    1982         3908 :             put_comment(p, e->lead_first + k);
    1983         3908 :             put_c(p, '\n');
    1984              :           }
    1985        42676 :         p_node(p, decls.items[i],
    1986        42676 :                (Ctx){.prec = 0, .ind = 0, .flat = false, .lead = true,
    1987              :                      .brk = false, .pad = 0});
    1988        42676 :         p_trail(p, decls.items[i], e);
    1989        42676 :         put_c(p, '\n');
    1990              :       }
    1991              :       // Rule 3 at the top level: a comment with no declaration after it.
    1992        17963 :       const WokTriviaEntry *fe = trivia_of(p, n);
    1993        17963 :       if (fe != nullptr)
    1994         6056 :         for (u32 k = 0; k < fe->trail_n; k++) {
    1995          441 :           put_comment(p, fe->trail_first + k);
    1996          441 :           put_c(p, '\n');
    1997              :         }
    1998        17963 :       return;
    1999              :     }
    2000              : 
    2001              :     case WOK_TAG_COUNT:
    2002              :       break;
    2003              :   }
    2004            0 :   WOK_UNREACHABLE();
    2005              : }
    2006              : 
    2007         3477 : static void p_if(Pr *p, const WokNode *n, Ctx c) {
    2008         3477 :   const WokNode *cond = E_If_cond(n);
    2009         3477 :   const WokNode *then_ = E_If_then_(n);
    2010         3477 :   const WokNode *else_ = E_If_else_(n);
    2011         3477 :   bool split = !c.flat && (opens_block(cond) || opens_block(then_));
    2012              : 
    2013         3477 :   put_z(p, "if ");
    2014              :   {
    2015          126 :     Ctx sc = split ? sub(c, EP_EXPR)
    2016         3477 :                    : sub_rest(c, EP_EXPR, 5 + body_cols(p, then_, c));
    2017         3477 :     if (split) sc.ind = c.ind + 1;
    2018         3477 :     p_node(p, cond, sc);
    2019              :   }
    2020         3477 :   if (!split) {
    2021         3351 :     put_z(p, " then");
    2022         3351 :     p_body(p, then_, c.ind, c.flat, 5 + body_cols(p, else_, c) + c.rest);
    2023         3351 :     put_z(p, " else");
    2024         3351 :     p_body(p, else_, c.ind, c.flat, c.rest);
    2025         3351 :     return;
    2026              :   }
    2027          126 :   nl(p, c.ind + 1, LN_LEAD);
    2028          126 :   put_z(p, "then");
    2029          126 :   p_body(p, then_, c.ind + 1, c.flat, 0);
    2030          126 :   nl(p, c.ind + 1, LN_LEAD);
    2031          126 :   put_z(p, "else");
    2032          126 :   p_body(p, else_, c.ind + 1, c.flat, c.rest);
    2033              : }
    2034              : 
    2035      1088440 : static void p_node(Pr *p, const WokNode *n, Ctx c) {
    2036      1088440 :   if (p->depth >= WOK_PRINT_MAX_DEPTH) {
    2037            0 :     unprintable(p, "a tree nested past the printer's depth cap");
    2038            0 :     put_z(p, "!DEPTH");
    2039            0 :     return;
    2040              :   }
    2041              :   // Measured BEFORE the depth is charged, so the measuring pass re-enters at
    2042              :   // the same depth this one is at and cannot exhaust the cap by halving it.
    2043      1088440 :   bool brk = decide_break(p, n, c);
    2044      1088440 :   p->depth++;
    2045      1088440 :   p->tstack[p->tn++] = n->tag;
    2046              : 
    2047      1088440 :   bool paren = node_prec(n) < c.prec || (c.lead && needs_lead_paren(n));
    2048        33986 :   if (paren) {
    2049        33986 :     put_c(p, '(');
    2050        33986 :     c.prec = 0;
    2051        33986 :     c.flat = true;  // a bracket group suspends layout: rule L2
    2052        33986 :     c.lead = false;
    2053              :   }
    2054      1088440 :   c.brk = brk;
    2055      1088440 :   p_node_inner(p, n, c);
    2056      1088440 :   if (paren) put_c(p, ')');
    2057              : 
    2058      1088440 :   p->tn--;
    2059      1088440 :   p->depth--;
    2060              : }
    2061              : 
    2062              : // ---------------------------------------------------------------- public
    2063              : 
    2064              : static const Ctx TOP = {.prec = 0, .ind = 0, .flat = false, .lead = true,
    2065              :                         .brk = false, .pad = 0};
    2066              : 
    2067            0 : void wok_print(const WokNode *file, const char *src, FILE *out) {
    2068            0 :   Pr p = {.to_file = true, .file = out, .src = src, .line = 1,
    2069            0 :           .tv = wok_trivia_of(file)};
    2070            0 :   p_node(&p, file, TOP);
    2071              :   // A W_File closes every declaration with a newline of its own.
    2072            0 :   if (file->tag != W_File) put_c(&p, '\n');
    2073            0 : }
    2074              : 
    2075          281 : char *wok_print_string(const WokNode *file, const char *src, WokArena *arena) {
    2076          281 :   return wok_print_string_named(file, src, arena, nullptr);
    2077              : }
    2078              : 
    2079        17963 : char *wok_print_string_named(const WokNode *file, const char *src,
    2080              :                              WokArena *arena, const char *path) {
    2081        35926 :   Pr p = {.arena = arena, .src = src, .path = path, .line = 1,
    2082        17963 :           .tv = wok_trivia_of(file)};
    2083        17963 :   p_node(&p, file, TOP);
    2084        17963 :   if (file->tag != W_File) put_c(&p, '\n');
    2085        17963 :   sink_reserve(&p, 1);
    2086        17963 :   p.buf[p.len] = '\0';
    2087        17963 :   return p.buf;
    2088              : }
        

Generated by: LCOV version 2.4-1


Machine-readable coverage data

This page is generated. If you are a tool, a script, or an LLM, read the JSON instead of scraping this HTML — it is the same measurement, exact, and it names the individual uncovered lines.

Every artifact here is stamped with the commit it measured and the run that produced it: commit 084deda, dated 2026-08-10T11:39:41Z, built by Github CI run. The totals in coverage.json are checked against lcov’s own summary before publishing, so the JSON and this page cannot disagree.