WokML compiler code coverage report (LCOV)
Current view: top level - c - wok_trivia.c (source / functions) Coverage Total Hit
Test: 084deda Lines: 98.9 % 280 277
Test Date: 2026-08-10 11:39:41 Functions: 100.0 % 24 24
Legend: Lines:     hit not hit

            Line data    Source code
       1              : // wok_trivia -- see wok_trivia.h for what this is and why comments are not
       2              : // slots.
       3              : //
       4              : // THE SHAPE OF THE PASS
       5              : //
       6              : //   1. index the source's lines, because every attachment rule is stated in
       7              : //      lines and columns and the token stream has already thrown the blank
       8              : //      ones away;
       9              : //   2. walk the tree once, in PRINT order, recording every block ITEM and
      10              : //      every block SCOPE with an Euler timestamp;
      11              : //   3. give each comment a target, in source order;
      12              : //   4. lay the comments out in one pooled array so each item's leading and
      13              : //      trailing runs are contiguous.
      14              : //
      15              : // STEP 2 IS THE ONE THAT PAYS FOR THE PROPERTY. The Euler tour gives every
      16              : // possible target a POSITION IN THE PRINTED TEXT, and step 3 refuses to let
      17              : // that position go backwards. That is what makes
      18              : //
      19              : //   comments(Format(t)) == comments(t)     -- same texts, SAME ORDER
      20              : //
      21              : // true by construction rather than by inspection of cases: a rule that would
      22              : // move a comment behind one that preceded it in the source is overridden and
      23              : // the comment joins its predecessor instead.
      24              : 
      25              : // The prelude comes FIRST: it carries the POSIX feature-test macros, which
      26              : // have no effect once a system header has been read. wok_base.h hard-errors
      27              : // if it is reached too late.
      28              : #include "wok_base.h"
      29              : 
      30              : #include "wok_trivia.h"
      31              : 
      32              : #include <string.h>
      33              : 
      34              : // The tree is bounded by the parser's own depth cap; this is only a guard so
      35              : // a hostile or damaged tree cannot walk off the C stack.
      36              : enum { TRIVIA_MAX_DEPTH = 512 };
      37              : 
      38              : #define NO_SCOPE UINT32_MAX
      39              : 
      40              : // ------------------------------------------------------------------ lines
      41              : 
      42              : typedef struct {
      43              :   const char *src;
      44              :   usize n;
      45              :   u32 *start;  // start[i] is the offset of line i, 0-based
      46              :   u32 nlines;
      47              : } Lines;
      48              : 
      49        20179 : static void lines_build(Lines *l, const char *src, usize n, WokArena *a) {
      50        20179 :   u32 count = 1;
      51      5891823 :   for (usize i = 0; i < n; i++)
      52      5871644 :     if (src[i] == '\n') count++;
      53        20179 :   l->src = src;
      54        20179 :   l->n = n;
      55        20179 :   l->nlines = count;
      56        20179 :   l->start = WOK_NEW_N(a, u32, count);
      57        20179 :   u32 k = 0;
      58        20179 :   l->start[k++] = 0;
      59      5891823 :   for (usize i = 0; i < n; i++)
      60      5871644 :     if (src[i] == '\n' && k < count) l->start[k++] = (u32)i + 1;
      61        20179 : }
      62              : 
      63       524120 : static u32 line_of(const Lines *l, u32 off) {
      64       524120 :   u32 lo = 0, hi = l->nlines - 1;
      65      3252958 :   while (lo < hi) {
      66      2204718 :     u32 mid = lo + (hi - lo + 1) / 2;
      67      2204718 :     if (l->start[mid] <= off)
      68              :       lo = mid;
      69              :     else
      70      1077280 :       hi = mid - 1;
      71              :   }
      72       524120 :   return lo;
      73              : }
      74              : 
      75       216760 : static u32 col_of(const Lines *l, u32 off) {
      76       216760 :   return off - l->start[line_of(l, off)] + 1;
      77              : }
      78              : 
      79        73914 : static bool is_blank_byte(char c) { return c == ' ' || c == '\t' || c == '\r'; }
      80              : 
      81              : // A line with nothing but whitespace on it -- the author's paragraph break.
      82        17586 : static bool line_is_blank(const Lines *l, u32 line) {
      83        17586 :   u32 from = l->start[line];
      84        17586 :   u32 to = line + 1 < l->nlines ? l->start[line + 1] : (u32)l->n;
      85        73465 :   for (u32 i = from; i < to; i++) {
      86        73465 :     char c = l->src[i];
      87        73465 :     if (c == '\n') break;
      88        73417 :     if (!is_blank_byte(c)) return false;
      89              :   }
      90              :   return true;
      91              : }
      92              : 
      93              : // Rule 1's test: is this the first non-whitespace on its line?
      94        49152 : static bool starts_its_line(const Lines *l, u32 off) {
      95        49436 :   for (u32 i = l->start[line_of(l, off)]; i < off; i++)
      96          497 :     if (!is_blank_byte(l->src[i])) return false;
      97              :   return true;
      98              : }
      99              : 
     100              : // ------------------------------------------------------- the layout table
     101              : //
     102              : // ONE switch decides two things the walk needs, so a new node cannot be added
     103              : // without answering both. -Wswitch with no `default:` is what enforces that.
     104              : //
     105              : //   block_slot -- the SEQ that wok_print writes with p_seq_block, whose
     106              : //                 members are therefore block ITEMS. Every other SEQ (tuple
     107              : //                 elements, constructor arguments) holds no items.
     108              : //   open_mask  -- the slots wok_print enters WITHOUT brackets, so an indented
     109              : //                 block can still exist under them. Inside brackets layout is
     110              : //                 suspended (rule L2) and no comment can be written on a line
     111              : //                 of its own, so items reached through any other slot are
     112              : //                 marked FLAT and never receive trivia. Forgetting a slot
     113              : //                 here is safe -- it only pushes a comment outward to an
     114              : //                 enclosing item; inventing one is not.
     115              : 
     116              : typedef struct {
     117              :   i32 block_slot;
     118              :   u32 open_mask;
     119              : } TagLayout;
     120              : 
     121              : #define SL(x) (1u << (x))
     122              : 
     123       597908 : static TagLayout tag_layout(WokTag t) {
     124       597908 :   switch (t) {
     125       515127 :     case N_Name:
     126              :     case N_ModPath:
     127              :     case D_Module:
     128              :     case D_Import:
     129              :     case D_Type:
     130              :     case D_Alias:
     131              :     case D_ExternType:
     132              :     case D_Sig:
     133              :     case D_Fixity:
     134              :     case H_FixRel:
     135              :     case D_Error:
     136              :     case H_TyParam:
     137              :     case H_ConDef:
     138              :     case H_FieldType:
     139              :     case H_OpSig:
     140              :     case H_ForeignMember:
     141              :     case H_SigName:
     142              :     case L_Prefix:
     143              :     case L_Infix:
     144              :     case T_Var:
     145              :     case T_Con:
     146              :     case T_App:
     147              :     case T_Fun:
     148              :     case T_Qual:
     149              :     case T_With:
     150              :     case T_List:
     151              :     case T_Tuple:
     152              :     case T_Unit:
     153              :     case T_RowArg:
     154              :     case T_Transfer:
     155              :     case H_RowEntry:
     156              :     case P_Var:
     157              :     case P_Wild:
     158              :     case P_Int:
     159              :     case P_Str:
     160              :     case P_Char:
     161              :     case P_Con:
     162              :     case P_Cons:
     163              :     case P_Tuple:
     164              :     case P_List:
     165              :     case P_Unit:
     166              :     case P_As:
     167              :     case P_Record:
     168              :     case H_FieldPat:
     169              :     case E_Var:
     170              :     case E_Con:
     171              :     case E_Int:
     172              :     case E_Str:
     173              :     case E_Char:
     174              :     case E_Unit:
     175              :     case E_OpRef:
     176              :     case E_App:
     177              :     case E_Chain:
     178              :     case E_Dot:
     179              :     case E_Neg:
     180              :     case E_List:
     181              :     case E_Tuple:
     182              :     case E_Record:
     183              :     case E_Error:
     184              :     case S_Use:
     185              :     case H_ChainOp:
     186              :     case H_UseBind:
     187              :     case H_Field:
     188              :     case WOK_TAG_COUNT:
     189       515127 :       return (TagLayout){-1, 0};
     190              : 
     191         4500 :     case D_Effect:
     192         4500 :       return (TagLayout){D_Effect__ops, SL(D_Effect__ops)};
     193          909 :     case D_Class:
     194          909 :       return (TagLayout){D_Class__body, SL(D_Class__body)};
     195         1004 :     case D_Instance:
     196         1004 :       return (TagLayout){D_Instance__body, SL(D_Instance__body)};
     197          883 :     case D_Foreign:
     198          883 :       return (TagLayout){D_Foreign__members, SL(D_Foreign__members)};
     199        20058 :     case D_Equation:
     200        20058 :       return (TagLayout){D_Equation__wheres,
     201              :                          SL(D_Equation__body) | SL(D_Equation__wheres)};
     202         1206 :     case E_Lambda:
     203         1206 :       return (TagLayout){-1, SL(E_Lambda__body)};
     204          548 :     case E_LetIn:
     205          548 :       return (TagLayout){-1, SL(E_LetIn__bind) | SL(E_LetIn__body)};
     206         1967 :     case E_HandleIn:
     207         1967 :       return (TagLayout){-1, SL(E_HandleIn__handler) | SL(E_HandleIn__body)};
     208          592 :     case E_UseIn:
     209          592 :       return (TagLayout){-1, SL(E_UseIn__body)};
     210          563 :     case E_If:
     211          563 :       return (TagLayout){
     212              :           -1, SL(E_If__cond) | SL(E_If__then_) | SL(E_If__else_)};
     213         3076 :     case E_Case:
     214         3076 :       return (TagLayout){E_Case__alts, SL(E_Case__scrut) | SL(E_Case__alts)};
     215         3415 :     case E_Handler:
     216         3415 :       return (TagLayout){E_Handler__clauses, SL(E_Handler__clauses)};
     217         1312 :     case E_Assign:
     218         1312 :       return (TagLayout){-1, SL(E_Assign__value)};
     219         3934 :     case E_Block:
     220         3934 :       return (TagLayout){E_Block__stmts, SL(E_Block__stmts)};
     221         2521 :     case S_Let:
     222         2521 :       return (TagLayout){-1, SL(S_Let__bind)};
     223         1160 :     case S_Handle:
     224         1160 :       return (TagLayout){-1, SL(S_Handle__handler)};
     225          216 :     case S_Discard:
     226          216 :       return (TagLayout){-1, SL(S_Discard__body)};
     227         3056 :     case H_Bind:
     228         3056 :       return (TagLayout){-1, SL(H_Bind__body)};
     229         5269 :     case H_Alt:
     230         5269 :       return (TagLayout){H_Alt__wheres, SL(H_Alt__body) | SL(H_Alt__wheres)};
     231         6413 :     case H_Clause:
     232         6413 :       return (TagLayout){-1, SL(H_Clause__body)};
     233        20179 :     case W_File:
     234        20179 :       return (TagLayout){W_File__decls, SL(W_File__decls)};
     235              :   }
     236            0 :   WOK_UNREACHABLE();
     237              : }
     238              : 
     239              : // A scope whose owner node can hold the comments that CLOSE it (rule 3) in
     240              : // its own trailing range. A `where` block is the exception: it has no node of
     241              : // its own -- its owner is the equation or alternative it hangs off, and that
     242              : // node's trailing range already belongs to the item itself. Its closing
     243              : // comments go to an enclosing block instead.
     244        63227 : static bool scope_is_closable(WokTag t) {
     245        63227 :   return t != D_Equation && t != H_Alt;
     246              : }
     247              : 
     248              : // The mirror of the above: an item may take a TRAILING comment only if its
     249              : // own trailing range is not already spoken for by a block it closes. An
     250              : // equation or alternative is safe even though it owns a `where`, because that
     251              : // block's closing comments were sent elsewhere.
     252         1352 : static bool item_takes_trail(WokTag t) {
     253         1352 :   return tag_layout(t).block_slot < 0 || !scope_is_closable(t);
     254              : }
     255              : 
     256         1164 : bool wok_trivia_trail_closes_block(const WokNode *n) {
     257         1164 :   return !item_takes_trail((WokTag)n->tag);
     258              : }
     259              : 
     260              : // ------------------------------------------------------- items and scopes
     261              : 
     262              : typedef struct {
     263              :   WokNode *node;
     264              :   u32 scope;
     265              :   u32 enter, exit;  // Euler timestamps: the item's place in the output
     266              :   u32 line, end_line, col;
     267              :   bool flat;
     268              :   bool first_in_scope;
     269              : } Item;
     270              : 
     271              : typedef struct {
     272              :   WokNode *owner;
     273              :   u32 parent;
     274              :   u32 item_col;
     275              :   u32 close;  // Euler timestamp of the point after the last item
     276              :   bool flat;
     277              :   bool closable;
     278              : } Scope;
     279              : 
     280              : typedef struct {
     281              :   WokArena *a;
     282              :   Lines lines;
     283              :   Item *item;
     284              :   u32 nitem, capitem;
     285              :   Scope *scope;
     286              :   u32 nscope, capscope;
     287              :   u32 clock;
     288              : } Builder;
     289              : 
     290       104506 : static u32 push_item(Builder *b, WokNode *n, u32 scope, bool flat,
     291              :                           bool first) {
     292       104506 :   if (b->nitem == b->capitem) {
     293        15928 :     u32 next = b->capitem ? b->capitem * 2 : 32;
     294        15928 :     Item *grown = WOK_NEW_N(b->a, Item, next);
     295        15928 :     if (b->nitem) memcpy(grown, b->item, (usize)b->nitem * sizeof *grown);
     296        15928 :     b->item = grown;
     297        15928 :     b->capitem = next;
     298              :   }
     299       104506 :   u32 last = n->len != 0 ? n->off + n->len - 1 : n->off;
     300       104506 :   if (last >= b->lines.n) last = b->lines.n != 0 ? (u32)b->lines.n - 1 : 0;
     301       209012 :   b->item[b->nitem] = (Item){.node = n,
     302              :                              .scope = scope,
     303              :                              .enter = 0,
     304              :                              .exit = 0,
     305       104506 :                              .line = line_of(&b->lines, n->off),
     306       104506 :                              .end_line = line_of(&b->lines, last),
     307       104506 :                              .col = col_of(&b->lines, n->off),
     308              :                              .flat = flat,
     309              :                              .first_in_scope = first};
     310       104506 :   return b->nitem++;
     311              : }
     312              : 
     313        63102 : static u32 push_scope(Builder *b, WokNode *owner, u32 parent,
     314              :                            bool flat, u32 item_col) {
     315        63102 :   if (b->nscope == b->capscope) {
     316        20659 :     u32 next = b->capscope ? b->capscope * 2 : 16;
     317        20659 :     Scope *grown = WOK_NEW_N(b->a, Scope, next);
     318        20659 :     if (b->nscope) memcpy(grown, b->scope, (usize)b->nscope * sizeof *grown);
     319        20659 :     b->scope = grown;
     320        20659 :     b->capscope = next;
     321              :   }
     322        63102 :   b->scope[b->nscope] = (Scope){.owner = owner,
     323              :                                 .parent = parent,
     324              :                                 .item_col = item_col,
     325              :                                 .close = 0,
     326              :                                 .flat = flat,
     327              :                                 .closable =
     328        63102 :                                     scope_is_closable((WokTag)owner->tag)};
     329        63102 :   return b->nscope++;
     330              : }
     331              : 
     332              : static void walk_node(Builder *b, WokNode *n, u32 scope, bool flat,
     333              :                       u32 depth);
     334              : 
     335        63102 : static void walk_scope(Builder *b, WokNode *owner, WokSeq seq, u32 parent,
     336              :                        bool flat, u32 depth) {
     337        35923 :   u32 col = seq.n != 0 ? col_of(&b->lines, seq.items[0]->off)
     338        63102 :                             : col_of(&b->lines, owner->off);
     339        63102 :   u32 s = push_scope(b, owner, parent, flat, col);
     340       167608 :   for (u32 i = 0; i < seq.n; i++) {
     341       104506 :     u32 idx = push_item(b, seq.items[i], s, flat, i == 0);
     342       104506 :     b->item[idx].enter = b->clock++;
     343       104506 :     walk_node(b, seq.items[i], s, flat, depth + 1);
     344       104506 :     b->item[idx].exit = b->clock++;
     345              :   }
     346        63102 :   b->scope[s].close = b->clock++;
     347        63102 : }
     348              : 
     349       596556 : static void walk_node(Builder *b, WokNode *n, u32 scope, bool flat,
     350              :                       u32 depth) {
     351       596556 :   if (depth >= TRIVIA_MAX_DEPTH) return;
     352       596556 :   TagLayout layout = tag_layout((WokTag)n->tag);
     353       596556 :   const WokNodeDesc *desc = &wok_node_desc[n->tag];
     354      1634369 :   for (u16 i = 0; i < desc->nfields; i++) {
     355      1037813 :     bool open = ((layout.open_mask >> i) & 1u) != 0;
     356      1037813 :     bool child_flat = flat || !open;
     357      1037813 :     switch (desc->fields[i].cls) {
     358       317981 :       case WFC_NODE:
     359              :       case WFC_OPT: {
     360       317981 :         WokNode *child = n->slot[i].node;
     361       317981 :         if (child != nullptr) walk_node(b, child, scope, child_flat, depth + 1);
     362              :         break;
     363              :       }
     364       233201 :       case WFC_SEQ: {
     365       233201 :         WokSeq seq = wok_seq_unpack(n->slot[i].seq);
     366       233201 :         if (layout.block_slot == (i32)i) {
     367        63102 :           walk_scope(b, n, seq, scope, child_flat, depth);
     368              :         } else {
     369       330747 :           for (u32 k = 0; k < seq.n; k++)
     370       160648 :             walk_node(b, seq.items[k], scope, child_flat, depth + 1);
     371              :         }
     372       233201 :         break;
     373              :       }
     374              :       case WFC_NAME:
     375              :       case WFC_TEXT:
     376              :       case WFC_INT:
     377              :       case WFC_FLAG:
     378              :       case WOK_FIELD_CLASS_COUNT:
     379              :         break;
     380              :     }
     381              :   }
     382              : }
     383              : 
     384              : // ------------------------------------------------------------- attachment
     385              : 
     386              : typedef enum { T_LEAD, T_TRAIL, T_CLOSE } TargetKind;
     387              : 
     388              : typedef struct {
     389              :   TargetKind kind;
     390              :   u32 idx;   // an item for T_LEAD/T_TRAIL, a scope for T_CLOSE
     391              :   u32 rank;  // where this lands in the printed text
     392              : } Target;
     393              : 
     394        49152 : static u32 target_rank(const Builder *b, TargetKind k, u32 idx) {
     395        49152 :   switch (k) {
     396        35620 :     case T_LEAD:
     397        35620 :       return b->item[idx].enter;
     398          188 :     case T_TRAIL:
     399          188 :       return b->item[idx].exit;
     400        13344 :     case T_CLOSE:
     401        13344 :       return b->scope[idx].close;
     402              :   }
     403            0 :   WOK_UNREACHABLE();
     404              : }
     405              : 
     406        49152 : static WokNode *target_node(const Builder *b, const Target *t) {
     407        49152 :   switch (t->kind) {
     408        35810 :     case T_LEAD:
     409              :     case T_TRAIL:
     410        35810 :       return b->item[t->idx].node;
     411        13342 :     case T_CLOSE:
     412        13342 :       return b->scope[t->idx].owner;
     413              :   }
     414            0 :   WOK_UNREACHABLE();
     415              : }
     416              : 
     417              : // Rule 2. The item this line's trailing comment belongs to is the last one
     418              : // that both begins and ENDS on the line, before the comment. An item that
     419              : // runs on -- because it opens a block whose own items follow -- is refused,
     420              : // because its trailing comment would be printed after that block and so after
     421              : // every comment written inside it.
     422              : // `before` is the count of items that begin left of the comment, which the
     423              : // caller advances monotonically; scanning back from it visits only the items
     424              : // on this one line.
     425          213 : static bool find_line_item(const Builder *b, u32 before, u32 line,
     426              :                            u32 coff, u32 *out) {
     427          221 :   for (u32 i = before; i-- > 0;) {
     428          213 :     const Item *it = &b->item[i];
     429          213 :     if (it->line < line) break;
     430          196 :     if (it->flat || it->end_line != line) continue;
     431          188 :     if (!item_takes_trail((WokTag)it->node->tag)) continue;
     432          188 :     if (it->node->off + it->node->len > coff) continue;
     433          188 :     *out = i;  // scanning backward, the first hit is the LAST on the line
     434          188 :     return true;
     435              :   }
     436              :   return false;
     437              : }
     438              : 
     439              : // Rule 3. The block a comment that follows no item belongs to is the
     440              : // innermost enclosing one whose ITEMS sit at or left of the comment. Column
     441              : // is the only evidence there is: the comment is inside whichever block its
     442              : // author indented it to.
     443        13361 : static u32 choose_close_scope(const Builder *b, u32 before,
     444              :                                    u32 ccol) {
     445        13361 :   u32 s = before != 0 ? b->item[before - 1].scope : NO_SCOPE;
     446        17129 :   while (s != NO_SCOPE) {
     447         8177 :     const Scope *sc = &b->scope[s];
     448         8177 :     if (sc->closable && !sc->flat && sc->item_col <= ccol) return s;
     449         3768 :     s = sc->parent;
     450              :   }
     451              :   return 0;  // scope 0 is the file's own: closable, never flat, column 1
     452              : }
     453              : 
     454              : // Is `outer` `inner` itself, or one of the blocks it nests inside?
     455           37 : static bool scope_encloses(const Builder *b, u32 outer, u32 inner) {
     456           61 :   while (inner != NO_SCOPE) {
     457           41 :     if (inner == outer) return true;
     458           24 :     inner = b->scope[inner].parent;
     459              :   }
     460              :   return false;
     461              : }
     462              : 
     463              : // ------------------------------------------------------- the entry table
     464              : //
     465              : // One entry per node that carries anything. Open addressing on the node
     466              : // pointer, sized so the table can never fill.
     467              : 
     468              : typedef struct {
     469              :   WokNode *key;
     470              :   u32 entry;
     471              : } MapSlot;
     472              : 
     473              : typedef struct {
     474              :   MapSlot *slot;
     475              :   u32 mask;
     476              :   WokTriviaEntry *entry;
     477              :   u32 nentry, capentry;
     478              : } EntryMap;
     479              : 
     480        69379 : static u32 ptr_hash(const void *p) {
     481        69379 :   u64 x = (u64)(uptr)p;
     482        69379 :   x ^= x >> 33;
     483        69379 :   x *= UINT64_C(0xFF51AFD7ED558CCD);
     484        69379 :   x ^= x >> 29;
     485        69379 :   return (u32)x;
     486              : }
     487              : 
     488        69379 : static u32 entry_for(EntryMap *m, WokNode *n) {
     489        69379 :   u32 i = ptr_hash(n) & m->mask;
     490        70460 :   while (m->slot[i].key != nullptr) {
     491        37965 :     if (m->slot[i].key == n) return m->slot[i].entry;
     492         1081 :     i = (i + 1) & m->mask;
     493              :   }
     494        32495 :   assert(m->nentry < m->capentry);  // sized for every possible target
     495        32495 :   u32 e = m->nentry++;
     496        32495 :   m->entry[e] = (WokTriviaEntry){0};
     497        32495 :   m->slot[i] = (MapSlot){.key = n, .entry = e};
     498        32495 :   n->trivia = e;
     499        32495 :   return e;
     500              : }
     501              : 
     502              : // --------------------------------------------------------------- bindings
     503              : //
     504              : // wok_print is handed a tree and the buffer its spans point into, and must
     505              : // find this table from those alone. The binding is keyed on the file node AND
     506              : // gated on `trivia != 0`, which only this pass ever sets: a node the arena
     507              : // later hands out at the same address is freshly zeroed, so a stale binding
     508              : // can never be mistaken for a live one. The tool is one-shot and
     509              : // single-threaded by design (design section 1), exactly as the coverage
     510              : // instrument in wok_ast.c already assumes.
     511              : 
     512              : enum { TRIVIA_BINDINGS = 8 };
     513              : 
     514              : static struct {
     515              :   const WokNode *file;
     516              :   const WokTrivia *table;
     517              : } bindings[TRIVIA_BINDINGS];
     518              : static u32 binding_next;
     519              : 
     520        20179 : static void bind_table(const WokNode *file, const WokTrivia *table) {
     521       179504 :   for (u32 i = 0; i < TRIVIA_BINDINGS; i++)
     522       159788 :     if (bindings[i].file == file) {
     523          463 :       bindings[i].table = table;
     524          463 :       return;
     525              :     }
     526        19716 :   u32 i = binding_next++ % TRIVIA_BINDINGS;
     527        19716 :   bindings[i].file = file;
     528        19716 :   bindings[i].table = table;
     529              : }
     530              : 
     531        17963 : const WokTrivia *wok_trivia_of(const WokNode *file) {
     532        17963 :   if (file == nullptr || file->trivia == 0) return nullptr;
     533        24077 :   for (u32 i = 0; i < TRIVIA_BINDINGS; i++)
     534        24077 :     if (bindings[i].file == file) return bindings[i].table;
     535              :   return nullptr;
     536              : }
     537              : 
     538              : // ------------------------------------------------------------------ attach
     539              : 
     540        20179 : const WokTrivia *wok_trivia_attach(WokNode *file, const char *src,
     541              :                                    usize src_len, const WokComment *comments,
     542              :                                    usize ncomments, WokArena *arena) {
     543        20179 :   WokTrivia *table = WOK_NEW(arena, WokTrivia);
     544        20179 :   *table = (WokTrivia){
     545              :       .entry = nullptr, .nentries = 0, .comment = nullptr, .ncomments = 0};
     546              : 
     547        20179 :   Builder b = {.a = arena,
     548              :                .item = nullptr,
     549              :                .nitem = 0,
     550              :                .capitem = 0,
     551              :                .scope = nullptr,
     552              :                .nscope = 0,
     553              :                .capscope = 0,
     554              :                .clock = 0};
     555        20179 :   lines_build(&b.lines, src, src_len, arena);
     556              :   // Scope 0 is the file's own block, and rule 3's last resort. A tree that is
     557              :   // not a whole file has no such block, so it takes no trivia at all.
     558        20179 :   if (file->tag == W_File) walk_node(&b, file, NO_SCOPE, false, 0);
     559        20179 :   if (b.nscope == 0) ncomments = 0;
     560              : 
     561              :   // Every node that can be a target, plus one so the file node always gets a
     562              :   // non-zero index -- that is what makes the binding check above sound.
     563        20179 :   u32 cap = (u32)ncomments + b.nitem + 2;
     564        20179 :   u32 size = 16;
     565        31188 :   while (size < cap * 2) size *= 2;
     566        20179 :   EntryMap map = {.slot = WOK_NEW_N(arena, MapSlot, size),
     567        20179 :                   .mask = size - 1,
     568        20179 :                   .entry = WOK_NEW_N(arena, WokTriviaEntry, cap),
     569              :                   .nentry = 0,
     570              :                   .capentry = cap};
     571        20179 :   memset(map.slot, 0, (usize)size * sizeof *map.slot);
     572        20179 :   map.entry[map.nentry++] = (WokTriviaEntry){0};  // the reserved sentinel
     573        20179 :   (void)entry_for(&map, file);
     574              : 
     575        20179 :   Target *target = WOK_NEW_N(arena, Target, ncomments + 1);
     576        20179 :   bool have_prev = false;
     577        20179 :   Target prev = {.kind = T_CLOSE, .idx = 0, .rank = 0};
     578              : 
     579              :   // Comments arrive in source order, so both cursors only ever move forward
     580              :   // and the whole pass is linear in items plus comments.
     581        20179 :   u32 before = 0;  // items that begin left of the comment
     582        20179 :   u32 next = 0;    // the first non-flat item that begins after it
     583              : 
     584        69331 :   for (usize ci = 0; ci < ncomments; ci++) {
     585        49152 :     const WokComment *c = &comments[ci];
     586        49152 :     u32 coff = c->off;
     587        49152 :     u32 cend = c->off + c->len;
     588        49152 :     u32 cline = line_of(&b.lines, coff);
     589        49152 :     u32 ccol = col_of(&b.lines, coff);
     590       105006 :     while (before < b.nitem && b.item[before].node->off < coff) before++;
     591       105006 :     while (next < b.nitem &&
     592        91578 :            (b.item[next].flat || b.item[next].node->off < cend))
     593        55854 :       next++;
     594        49152 :     bool have_next = next < b.nitem;
     595              : 
     596        49152 :     Target t = {.kind = T_CLOSE, .idx = 0, .rank = 0};
     597        49152 :     u32 idx = 0;
     598        49152 :     bool decided = false;
     599              : 
     600        49365 :     if (!starts_its_line(&b.lines, coff) &&
     601          213 :         find_line_item(&b, before, cline, coff, &idx)) {
     602          188 :       t.kind = T_TRAIL;
     603          188 :       t.idx = idx;
     604          188 :       decided = true;
     605              :     }
     606        49152 :     if (!decided && have_next && ccol <= b.item[next].col) {
     607              :       // Deeper than the item it precedes means the author left it inside the
     608              :       // block that just ended, not attached to what comes next.
     609              :       t.kind = T_LEAD;
     610              :       t.idx = next;
     611              :       decided = true;
     612              :     }
     613        13549 :     if (!decided) {
     614        13361 :       u32 s = choose_close_scope(&b, before, ccol);
     615              :       // A block only CLOSES if what follows is outside it. Between two items
     616              :       // of the same block, an over-indented comment is still that block's --
     617              :       // it leads the item it was written above.
     618        13361 :       if (have_next && scope_encloses(&b, s, b.item[next].scope)) {
     619              :         t.kind = T_LEAD;
     620              :         t.idx = next;
     621              :       } else {
     622              :         t.kind = T_CLOSE;
     623              :         t.idx = s;
     624              :       }
     625              :     }
     626        49152 :     t.rank = target_rank(&b, t.kind, t.idx);
     627              : 
     628              :     // The order guarantee. A rule that would print this comment before one
     629              :     // that preceded it in the source loses; the comment joins its predecessor.
     630        49152 :     if (have_prev && t.rank < prev.rank) t = prev;
     631        49152 :     target[ci] = t;
     632        49152 :     prev = t;
     633        49152 :     have_prev = true;
     634              :   }
     635              : 
     636              :   // blank_before, read off the source: the paragraph break above the item,
     637              :   // counting a leading comment block as part of the item.
     638        20179 :   u32 *first_lead = WOK_NEW_N(arena, u32, b.nitem + 1);
     639       144864 :   for (u32 i = 0; i <= b.nitem; i++) first_lead[i] = UINT32_MAX;
     640        69331 :   for (usize ci = ncomments; ci-- > 0;)
     641        49152 :     if (target[ci].kind == T_LEAD) first_lead[target[ci].idx] = (u32)ci;
     642              : 
     643       124685 :   for (u32 i = 0; i < b.nitem; i++) {
     644       104506 :     const Item *it = &b.item[i];
     645              :     // Only a break BETWEEN two items of one block is printable: the top level
     646              :     // already writes its own blank lines, and nothing precedes a block's
     647              :     // first item.
     648       104506 :     if (it->flat || it->first_in_scope || it->scope == 0) continue;
     649        17542 :     u32 line = first_lead[i] != UINT32_MAX
     650           44 :                         ? line_of(&b.lines, comments[first_lead[i]].off)
     651        17586 :                         : it->line;
     652        17586 :     if (line == 0 || !line_is_blank(&b.lines, line - 1)) continue;
     653           48 :     map.entry[entry_for(&map, it->node)].blank_before = 1;
     654              :   }
     655              : 
     656              :   // The pool. Ranks never decrease, so equal-ranked comments are adjacent and
     657              :   // every run below is contiguous without a second pass to prove it.
     658        20179 :   WokComment *pool = WOK_NEW_N(arena, WokComment, ncomments + 1);
     659        69331 :   for (usize ci = 0; ci < ncomments; ci++) {
     660        49152 :     WokTriviaEntry *e = &map.entry[entry_for(&map, target_node(&b,
     661        49152 :                                                                &target[ci]))];
     662        49152 :     if (target[ci].kind == T_LEAD) {
     663        35622 :       if (e->lead_n == 0) e->lead_first = (u32)ci;
     664        35622 :       e->lead_n++;
     665              :     } else {
     666        13530 :       if (e->trail_n == 0) e->trail_first = (u32)ci;
     667        13530 :       e->trail_n++;
     668              :     }
     669        49152 :     pool[ci] = comments[ci];
     670              :   }
     671              : 
     672        20179 :   table->entry = map.entry;
     673        20179 :   table->nentries = map.nentry;
     674        20179 :   table->comment = pool;
     675        20179 :   table->ncomments = (u32)ncomments;
     676        20179 :   bind_table(file, table);
     677        20179 :   return table;
     678              : }
        

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.