WokML compiler code coverage report (LCOV)
Current view: top level - c - wok_resolve.c (source / functions) Coverage Total Hit
Test: 084deda Lines: 98.2 % 625 614
Test Date: 2026-08-10 11:39:41 Functions: 100.0 % 37 37
Legend: Lines:     hit not hit

            Line data    Source code
       1              : // wok_resolve -- see wok_resolve.h.
       2              : 
       3              : #include "wok_resolve.h"
       4              : 
       5              : #include <stdarg.h>
       6              : #include <string.h>
       7              : 
       8              : // ---------------------------------------------------------- the effect table
       9              : //
      10              : // Two passes over the file's declarations: count, then fill. The alternative,
      11              : // a growable buffer, would buy nothing -- a file's effects are all declared
      12              : // before anything is checked, so there is no incremental case to serve.
      13              : 
      14              : typedef struct {
      15              :   WokSpan name;
      16              :   u32 arity;
      17              :   bool known;  // false when the type goes through something this file
      18              :                // cannot expand -- then the count would be a guess, so no
      19              :                // clause is checked against it
      20              : } Op;
      21              : 
      22              : // A same-file `alias` can put arrows behind a name (`alias Setter = U64 ->
      23              : // ()`), so the arity walk must see through it or a correct clause gets a
      24              : // confidently wrong count.
      25              : typedef struct {
      26              :   WokSpan name;
      27              :   const WokNode *body;
      28              :   bool nullary;  // only a parameterless alias is expandable by substitution
      29              : } Alias;
      30              : 
      31              : typedef struct {
      32              :   WokSpan name;
      33              :   Op *ops;
      34              :   u32 nops;
      35              : } Eff;
      36              : 
      37              : // ---------------------------------------------------------- the fixity table
      38              : //
      39              : // wok's fixity is a PARTIAL ORDER, not a ladder of numbered levels: `fixity *
      40              : // left tighter than +` states one edge, and two operators with no path
      41              : // between them are INCOMPARABLE -- which is a fault at the use site, never a
      42              : // tie broken by a default. `Equal` means the same operator, so associativity
      43              : // only ever settles a run of one.
      44              : //
      45              : // The order is stored as a bit matrix and closed ONCE with Warshall, rather
      46              : // than answered per query by a graph search. That is not for speed at this
      47              : // size; it is because the closure answers the two questions this stage has
      48              : // with the same bits. Comparison is `bit(a,b)` / `bit(b,a)`, and a CYCLE is
      49              : // `bit(a,a)` -- a node that reaches itself -- so the circularity check needs
      50              : // no second algorithm and self-reference is just its one-node case.
      51              : // FixOp is WokFixTable's element type (wok_resolve.h): the table this stage
      52              : // builds IS the handle the reorder pass consults, so there is one struct, not
      53              : // a private one shaped like a public one. `decl_off` doubles as "where it was
      54              : // declared" for the already-has-a-fixity diagnostic below.
      55              : typedef WokFixOp FixOp;
      56              : 
      57              : typedef struct {
      58              :   FixOp *ops;
      59              :   u32 n;
      60              :   u64 *edge;   // n rows of `words` u64s; bit (i,j) = i binds tighter than j
      61              :   u32 words;
      62              : } Fix;
      63              : 
      64              : // ------------------------------------------------------------- name index
      65              : //
      66              : // The three declaration tables (effects, fixity operators, aliases) used to
      67              : // be searched by linear scan. Fine at hand-written sizes, QUADRATIC the
      68              : // moment a table grows with the file: a 20k-effect file spent three quarters
      69              : // of its whole runtime in find_effect's memcmps. One small open-addressing
      70              : // index serves all three -- FNV-1a over the name's bytes, power-of-two
      71              : // capacity at load <= 1/2 (so probing always terminates), parallel key/entry
      72              : // arrays, and no deletion because nothing is ever removed from a table.
      73              : // "First declaration wins" holds by construction: a name is added only when
      74              : // it is not already present.
      75              : typedef struct {
      76              :   WokSpan *key;  // key[k] = the name stored at slot k
      77              :   u32 *entry;    // entry[k] = index into the backing table; UINT32_MAX free
      78              :   u32 mask;
      79              : } NameIdx;
      80              : 
      81              : typedef struct {
      82              :   const char *src;
      83              :   WokDiagSink *d;
      84              :   const WokNode *file;
      85              :   WokArena *arena;
      86              :   Eff *effs;
      87              :   u32 neffs;
      88              :   Alias *aliases;
      89              :   u32 naliases;
      90              :   Fix fix;
      91              :   NameIdx eff_idx;
      92              :   NameIdx fix_idx;
      93              :   NameIdx alias_idx;
      94              :   const char *fix_note;  // the declared order, rendered once on first fault
      95              : } R;
      96              : 
      97          253 : static bool fix_bit(const Fix *f, u32 i, u32 j) {
      98          253 :   return (f->edge[(usize)i * f->words + j / 64] >> (j % 64) & 1u) != 0;
      99              : }
     100              : 
     101           25 : static void fix_set(Fix *f, u32 i, u32 j) {
     102           25 :   f->edge[(usize)i * f->words + j / 64] |= UINT64_C(1) << (j % 64);
     103           25 : }
     104              : 
     105          500 : static bool span_eq(const R *r, WokSpan a, WokSpan b) {
     106          500 :   return a.len == b.len && memcmp(r->src + a.off, r->src + b.off, a.len) == 0;
     107              : }
     108              : 
     109          591 : static u32 name_hash(const R *r, WokSpan s) {
     110          591 :   u32 h = 2166136261u;  // FNV-1a
     111         1653 :   for (u32 i = 0; i < s.len; i++) {
     112         1062 :     h ^= (unsigned char)r->src[s.off + i];
     113         1062 :     h *= 16777619u;
     114              :   }
     115          591 :   return h;
     116              : }
     117              : 
     118              : // max_entries is an UPPER BOUND known before any insert (every table counts
     119              : // its declarations first), so capacity is fixed and there is no rehash.
     120          262 : static void idx_init(NameIdx *ix, WokArena *a, u32 max_entries) {
     121          262 :   u32 cap = 8;
     122          269 :   while (cap < max_entries * 2) cap *= 2;
     123          262 :   ix->key = WOK_NEW_N(a, WokSpan, cap);
     124          262 :   ix->entry = WOK_NEW_N(a, u32, cap);
     125          262 :   ix->mask = cap - 1;
     126         2422 :   for (u32 i = 0; i < cap; i++) ix->entry[i] = UINT32_MAX;
     127          262 : }
     128              : 
     129          467 : static u32 idx_find(const R *r, const NameIdx *ix, WokSpan name) {
     130          467 :   for (u32 k = name_hash(r, name) & ix->mask;; k = (k + 1) & ix->mask) {
     131          467 :     if (ix->entry[k] == UINT32_MAX) return UINT32_MAX;
     132          332 :     if (span_eq(r, ix->key[k], name)) return ix->entry[k];
     133              :   }
     134              : }
     135              : 
     136              : // The caller has already established absence (every table does a find first,
     137              : // because a duplicate is either reported or skipped before anything is
     138              : // added), so this only claims a free slot.
     139          124 : static void idx_add(const R *r, NameIdx *ix, WokSpan name, u32 entry) {
     140          124 :   u32 k = name_hash(r, name) & ix->mask;
     141          124 :   while (ix->entry[k] != UINT32_MAX) k = (k + 1) & ix->mask;
     142          124 :   ix->key[k] = name;
     143          124 :   ix->entry[k] = entry;
     144          124 : }
     145              : 
     146              : // A NAME span quoted into a message. Spans view the source and are not
     147              : // NUL-terminated, so every message that names something goes through `%.*s`.
     148              : #define SPAN(r, s) (int)(s).len, (r)->src + (s).off
     149              : 
     150          117 : static void build_aliases(R *r, const WokNode *file, WokArena *a) {
     151          117 :   WokSeq decls = W_File_decls(file);
     152          117 :   u32 n = 0;
     153          798 :   for (u32 i = 0; i < decls.n; i++)
     154          681 :     if (decls.items[i]->tag == D_Alias) n++;
     155          117 :   r->naliases = 0;
     156          117 :   r->aliases = n ? WOK_NEW_N(a, Alias, n) : nullptr;
     157          117 :   idx_init(&r->alias_idx, a, n);
     158          798 :   for (u32 i = 0; i < decls.n; i++) {
     159          681 :     const WokNode *d = decls.items[i];
     160          681 :     if (d->tag != D_Alias) continue;
     161            6 :     WokSpan name = D_Alias_name(d);
     162              :     // First declaration wins, as everywhere: a redeclared alias is skipped.
     163            6 :     if (idx_find(r, &r->alias_idx, name) != UINT32_MAX) continue;
     164            6 :     idx_add(r, &r->alias_idx, name, r->naliases);
     165           12 :     r->aliases[r->naliases++] =
     166            6 :         (Alias){.name = name, .body = D_Alias_body(d),
     167            6 :                 .nullary = D_Alias_params(d).n == 0};
     168              :   }
     169          117 : }
     170              : 
     171              : // The single unqualified name a T_Con spells, or a zero span. A qualified
     172              : // path is left alone: it cannot name a same-file alias.
     173           68 : static WokSpan tcon_bare_name(const WokNode *t) {
     174           68 :   const WokNode *path = T_Con_path(t);
     175           68 :   if (path && path->tag == N_ModPath) {
     176           68 :     WokSeq parts = N_ModPath_parts(path);
     177           68 :     if (parts.n == 1 && parts.items[0]->tag == N_Name)
     178           68 :       return N_Name_text(parts.items[0]);
     179              :   }
     180            0 :   return wok_span(0, 0);
     181              : }
     182              : 
     183           68 : static const Alias *find_alias(const R *r, WokSpan name) {
     184           68 :   if (name.len == 0) return nullptr;
     185           68 :   u32 i = idx_find(r, &r->alias_idx, name);
     186           68 :   return i == UINT32_MAX ? nullptr : &r->aliases[i];
     187              : }
     188              : 
     189              : // The op's ARITY is the length of its type's arrow spine. `get : s` takes
     190              : // none, `set : s -> ()` takes one, and `call : (U64 -> U64) -> U64` takes one
     191              : // -- the argument's own arrow is inside a bracket, so it is not on the spine,
     192              : // which is exactly the distinction reject/02 turns on.
     193              : //
     194              : // A row (`op : a -> b with E`) or a context (`Eq a => ...`) wraps the type
     195              : // without changing its spine, so both are stepped through rather than
     196              : // counted. A terminal that names a same-file parameterless alias is expanded
     197              : // and the walk continues -- the alias may hide more arrows. A parameterized
     198              : // or applied alias would need substitution to expand, so the arity is
     199              : // UNKNOWN (false) and the caller checks nothing: a skipped check is the
     200              : // partial-knowledge answer, a guessed count is a wrong E-ARITY on valid
     201              : // code. The fuel bounds alias cycles (`alias A = A`), which also come out
     202              : // unknown rather than hanging the walk.
     203           73 : static bool arrow_arity(const R *r, const WokNode *t, u32 *out) {
     204           73 :   u32 n = 0;
     205           73 :   u32 fuel = 32;
     206          161 :   while (t) {
     207          161 :     if (t->tag == T_Fun) {
     208           54 :       n++;
     209           54 :       t = T_Fun_to(t);
     210          107 :     } else if (t->tag == T_With) {
     211            0 :       t = T_With_body(t);
     212          107 :     } else if (t->tag == T_Qual) {
     213            0 :       t = T_Qual_body(t);
     214              :     } else {
     215              :       const WokNode *head = t;
     216              :       bool applied = false;
     217          108 :       while (head && head->tag == T_App) {
     218            1 :         head = T_App_fn(head);
     219            1 :         applied = true;
     220              :       }
     221          107 :       const Alias *al = head && head->tag == T_Con
     222           68 :                             ? find_alias(r, tcon_bare_name(head))
     223          107 :                             : nullptr;
     224           68 :       if (!al) break;  // a data type: the spine ends here
     225           36 :       if (applied || !al->nullary || fuel-- == 0) return false;
     226           34 :       t = al->body;
     227              :     }
     228              :   }
     229           71 :   *out = n;
     230           71 :   return true;
     231              : }
     232              : 
     233           64 : static Eff *find_effect_mut(R *r, WokSpan name) {
     234           64 :   u32 i = idx_find(r, &r->eff_idx, name);
     235           64 :   return i == UINT32_MAX ? nullptr : &r->effs[i];
     236              : }
     237              : 
     238              : // TWO NAMESPACES, and the table's shape says which is which. An EFFECT name
     239              : // is global to the file, so a second declaration of it is a redeclaration.
     240              : // An OP name is scoped INSIDE its effect -- it is reached as `State.get`, or
     241              : // ambiently through a label that names the effect -- so `State.get` and
     242              : // `Reader.get` are two different ops that happen to share a spelling, and
     243              : // nothing here compares one effect's ops against another's.
     244              : //
     245              : // The duplicate is reported and DROPPED rather than appended, which is what
     246              : // makes "first declaration wins" a property of the table rather than of the
     247              : // order find_effect happens to scan in.
     248          117 : static void build_table(R *r, const WokNode *file, WokArena *a) {
     249          117 :   WokSeq decls = W_File_decls(file);
     250          117 :   u32 n = 0;
     251          798 :   for (u32 i = 0; i < decls.n; i++)
     252          681 :     if (decls.items[i]->tag == D_Effect) n++;
     253          117 :   r->neffs = 0;
     254          117 :   r->effs = n ? WOK_NEW_N(a, Eff, n) : nullptr;
     255          117 :   idx_init(&r->eff_idx, a, n);
     256          798 :   for (u32 i = 0; i < decls.n; i++) {
     257          681 :     const WokNode *d = decls.items[i];
     258          683 :     if (d->tag != D_Effect) continue;
     259           64 :     WokSpan name = D_Effect_name(d);
     260           64 :     const Eff *first = find_effect_mut(r, name);
     261           64 :     if (first) {
     262            2 :       u32 line = 0, col = 0;
     263            2 :       wok_diag_position(r->d, first->name.off, &line, &col);
     264            2 :       wok_diag_add(r->d, WOK_E_DUPLICATE, name.off, name.len,
     265            2 :                    "effect `%.*s` is already declared at %u:%u", SPAN(r, name),
     266              :                    line, col);
     267            2 :       continue;
     268              :     }
     269           62 :     WokSeq ops = D_Effect_ops(d);
     270           62 :     idx_add(r, &r->eff_idx, name, r->neffs);
     271           62 :     Eff *e = &r->effs[r->neffs++];
     272           62 :     e->name = name;
     273           62 :     e->nops = 0;
     274           62 :     e->ops = ops.n ? WOK_NEW_N(a, Op, ops.n) : nullptr;
     275          135 :     for (u32 j = 0; j < ops.n; j++) {
     276           73 :       if (ops.items[j]->tag != H_OpSig) continue;
     277           73 :       Op *op = &e->ops[e->nops++];
     278           73 :       op->name = H_OpSig_name(ops.items[j]);
     279           73 :       op->arity = 0;
     280           73 :       op->known = arrow_arity(r, H_OpSig_type(ops.items[j]), &op->arity);
     281              :     }
     282              :   }
     283          117 : }
     284              : 
     285           61 : static const Eff *find_effect(const R *r, WokSpan name) {
     286           61 :   u32 i = idx_find(r, &r->eff_idx, name);
     287           61 :   return i == UINT32_MAX ? nullptr : &r->effs[i];
     288              : }
     289              : 
     290              : // ------------------------------------------------------- fixity, built once
     291              : 
     292              : // Operator identity is the SPELLING, and `alpha` is not part of it: an
     293              : // identifier and a symbol run are drawn from disjoint character sets, so two
     294              : // operators that spell the same are the same operator, whichever way they
     295              : // were written.
     296          296 : static u32 fix_find(const R *r, WokSpan name) {
     297              :   // Guarded: build_fixity returns before initialising the index when the
     298              :   // file declares no fixity at all, and use sites still ask.
     299          296 :   if (r->fix.n == 0) return UINT32_MAX;
     300          268 :   return idx_find(r, &r->fix_idx, name);
     301              : }
     302              : 
     303              : // The WINNER predicate, in one place: whether this `fixity` declaration is
     304              : // the one the table kept (a dropped duplicate contributes nothing). Pass 2's
     305              : // edges and the diagnostic's printed table both ask THIS function, because
     306              : // "the table printed is the table consulted" is only a guarantee while the
     307              : // two cannot drift -- and F5 is already specced to change this rule to an
     308              : // origin-keyed merge, which must then change it for both askers at once.
     309           72 : static bool fix_is_winner(const R *r, const WokNode *d) {
     310           72 :   u32 head = fix_find(r, D_Fixity_name(d));
     311           72 :   return head != UINT32_MAX &&
     312           72 :          r->fix.ops[head].decl_off == D_Fixity_name(d).off;
     313              : }
     314              : 
     315          117 : static void build_fixity(R *r, const WokNode *file, WokArena *a) {
     316          117 :   WokSeq decls = W_File_decls(file);
     317          117 :   u32 cap = 0;
     318          798 :   for (u32 i = 0; i < decls.n; i++) {
     319          681 :     if (decls.items[i]->tag != D_Fixity) continue;
     320           55 :     cap += 1 + D_Fixity_rels(decls.items[i]).n;  // the head, and every neighbour
     321              :   }
     322          117 :   if (cap == 0) return;
     323           28 :   r->fix.ops = WOK_NEW_N(a, FixOp, cap);
     324           28 :   r->fix.n = 0;
     325           28 :   idx_init(&r->fix_idx, a, cap);
     326              : 
     327              :   // Pass 1: every operator NAMED anywhere by a WINNING declaration gets an
     328              :   // index, so a relation can point at one declared later in the file. Only a
     329              :   // `fixity` head carries an associativity; a name that appears solely as a
     330              :   // neighbour is a placeholder and stays out of the redeclaration check.
     331              :   //
     332              :   // A dropped duplicate contributes NOTHING -- not edges (pass 2), and not
     333              :   // placeholders either: a neighbour named only by the loser would otherwise
     334              :   // sit in the table edge-less, read as known-but-incomparable at use sites
     335              :   // the winner's table would leave unknown and skip.
     336          166 :   for (u32 i = 0; i < decls.n; i++) {
     337          138 :     const WokNode *d = decls.items[i];
     338          141 :     if (d->tag != D_Fixity) continue;
     339           55 :     WokSpan name = D_Fixity_name(d);
     340           55 :     u32 at = fix_find(r, name);
     341           55 :     if (at != UINT32_MAX && r->fix.ops[at].decl_off != UINT32_MAX) {
     342            3 :       u32 line = 0, col = 0;
     343            3 :       wok_diag_position(r->d, r->fix.ops[at].decl_off, &line, &col);
     344            3 :       wok_diag_add(r->d, WOK_E_DUPLICATE, name.off, name.len,
     345              :                    "operator `%.*s` already has a fixity at %u:%u",
     346            3 :                    SPAN(r, name), line, col);
     347            3 :       continue;
     348              :     }
     349            3 :     if (at != UINT32_MAX) {
     350            3 :       r->fix.ops[at].assoc = D_Fixity_assoc(d);
     351            3 :       r->fix.ops[at].decl_off = name.off;
     352              :     } else {
     353           49 :       idx_add(r, &r->fix_idx, name, r->fix.n);
     354           49 :       r->fix.ops[r->fix.n++] = (FixOp){.name = name,
     355           49 :                                        .assoc = D_Fixity_assoc(d),
     356              :                                        .decl_off = name.off};
     357              :     }
     358           52 :     WokSeq rels = D_Fixity_rels(d);
     359           77 :     for (u32 j = 0; j < rels.n; j++) {
     360           25 :       WokSpan nb = H_FixRel_name(rels.items[j]);
     361           25 :       if (fix_find(r, nb) != UINT32_MAX) continue;
     362            7 :       idx_add(r, &r->fix_idx, nb, r->fix.n);
     363            7 :       r->fix.ops[r->fix.n++] =
     364              :           (FixOp){.name = nb, .assoc = WOK_ASSOC_LEFT, .decl_off = UINT32_MAX};
     365              :     }
     366              :   }
     367              : 
     368              :   // Pass 2: the edges. `a tighter than b` and `b looser than a` are the same
     369              :   // edge stated from opposite ends, which is why the sense is recorded rather
     370              :   // than normalised away at parse time -- the spelling is the author's.
     371           28 :   r->fix.words = (r->fix.n + 63) / 64;
     372           28 :   r->fix.edge = WOK_NEW_N(a, u64, (usize)r->fix.n * r->fix.words);
     373           84 :   for (usize i = 0; i < (usize)r->fix.n * r->fix.words; i++) r->fix.edge[i] = 0;
     374          166 :   for (u32 i = 0; i < decls.n; i++) {
     375          138 :     const WokNode *d = decls.items[i];
     376          141 :     if (d->tag != D_Fixity) continue;
     377              :     // A dropped duplicate's edges die with it: first declaration wins the
     378              :     // EDGES, not only the associativity, which is what the E-DUPLICATE
     379              :     // message promised.
     380           55 :     if (!fix_is_winner(r, d)) continue;
     381           52 :     u32 head = fix_find(r, D_Fixity_name(d));
     382           52 :     WokSeq rels = D_Fixity_rels(d);
     383           77 :     for (u32 j = 0; j < rels.n; j++) {
     384           25 :       u32 nb = fix_find(r, H_FixRel_name(rels.items[j]));
     385           25 :       if (nb == UINT32_MAX) continue;
     386           25 :       if (H_FixRel_sense(rels.items[j]) == WOK_FIXREL_TIGHTER)
     387           22 :         fix_set(&r->fix, head, nb);
     388              :       else
     389            3 :         fix_set(&r->fix, nb, head);
     390              :     }
     391              :   }
     392              : 
     393              :   // Warshall, one row-OR per (k, i): if i reaches k, i reaches everything k
     394              :   // does. O(n^3/64) on words, over an n that is the operator count of one
     395              :   // file.
     396           84 :   for (u32 k = 0; k < r->fix.n; k++)
     397          196 :     for (u32 i = 0; i < r->fix.n; i++) {
     398          140 :       if (!fix_bit(&r->fix, i, k)) continue;
     399           60 :       for (u32 w = 0; w < r->fix.words; w++)
     400           30 :         r->fix.edge[(usize)i * r->fix.words + w] |=
     401           30 :             r->fix.edge[(usize)k * r->fix.words + w];
     402              :     }
     403              : }
     404              : 
     405              : // A fixity fault carries the order the checker was using, as continuation
     406              : // lines: one verbatim source slice per WINNING `fixity` declaration, in
     407              : // declaration order. Verbatim, because the table printed must be the table
     408              : // consulted -- the author's own spelling of `tighter`/`looser`, and a
     409              : // dropped duplicate absent because it is not in the table. Built once per
     410              : // file, on the first fault; fault-free files never pay for it.
     411            7 : static const char *fixity_note(R *r) {
     412            7 :   if (r->fix_note) return r->fix_note;
     413            7 :   static const char header[] = "\n  the order this file declares:";
     414            7 :   static const char nb[] = " (named only as a neighbour)";
     415              :   // The WALK happens once, into line records; measuring and filling are then
     416              :   // dumb loops over the records, so the winner predicate and the byte
     417              :   // accounting cannot drift apart. A line is the verbatim source of one
     418              :   // winning declaration, or -- because the table printed must be everything
     419              :   // the table knows -- the bare name of an operator known only from someone
     420              :   // else's `tighter than` clause, marked as such.
     421            7 :   typedef struct {
     422              :     WokSpan text;
     423              :     bool neighbour;
     424              :   } Line;
     425            7 :   WokSeq decls = W_File_decls(r->file);
     426            7 :   Line *lines = WOK_NEW_N(r->arena, Line, decls.n + r->fix.n);
     427            7 :   u32 nlines = 0;
     428           45 :   for (u32 i = 0; i < decls.n; i++) {
     429           38 :     const WokNode *d = decls.items[i];
     430           38 :     if (d->tag != D_Fixity) continue;
     431           17 :     if (!fix_is_winner(r, d)) continue;
     432           16 :     lines[nlines++] = (Line){.text = wok_span(d->off, d->len),
     433              :                              .neighbour = false};
     434              :   }
     435           23 :   for (u32 i = 0; i < r->fix.n; i++)
     436           16 :     if (r->fix.ops[i].decl_off == UINT32_MAX)
     437            0 :       lines[nlines++] = (Line){.text = r->fix.ops[i].name, .neighbour = true};
     438              : 
     439              :   usize len = sizeof header - 1;
     440           23 :   for (u32 i = 0; i < nlines; i++)
     441           32 :     len += 5 + lines[i].text.len + (lines[i].neighbour ? sizeof nb - 1 : 0);
     442            7 :   char *note = WOK_NEW_N(r->arena, char, len + 1);
     443            7 :   usize at = sizeof header - 1;
     444            7 :   memcpy(note, header, at);
     445           23 :   for (u32 i = 0; i < nlines; i++) {
     446           16 :     memcpy(note + at, "\n    ", 5);
     447           16 :     memcpy(note + at + 5, r->src + lines[i].text.off, lines[i].text.len);
     448           16 :     at += 5 + lines[i].text.len;
     449           16 :     if (lines[i].neighbour) {
     450            0 :       memcpy(note + at, nb, sizeof nb - 1);
     451            0 :       at += sizeof nb - 1;
     452              :     }
     453              :   }
     454            7 :   note[at] = '\0';
     455            7 :   r->fix_note = note;
     456            7 :   return note;
     457              : }
     458              : 
     459              : // An E-FIXITY with the note appended. The head is bounded (a message and at
     460              : // most a ring of names); the note is not, so the assembled message goes to
     461              : // the sink through the non-formatting path rather than a format buffer.
     462              : WOK_PRINTF(4, 5)
     463            7 : static void diag_fixity(R *r, u32 off, u32 len, const char *fmt, ...) {
     464            7 :   char head[768];
     465            7 :   va_list ap;
     466            7 :   va_start(ap, fmt);
     467            7 :   (void)vsnprintf(head, sizeof head, fmt, ap);
     468            7 :   va_end(ap);
     469            7 :   const char *note = fixity_note(r);
     470            7 :   usize nhead = strlen(head), nnote = strlen(note);
     471            7 :   char *msg = WOK_NEW_N(r->arena, char, nhead + nnote + 1);
     472            7 :   memcpy(msg, head, nhead);
     473            7 :   memcpy(msg + nhead, note, nnote + 1);
     474            7 :   wok_diag_add_text(r->d, WOK_E_FIXITY, off, len, msg);
     475            7 : }
     476              : 
     477              : // A CYCLE is a node that reaches itself, and the closure has already found
     478              : // every one. Reported once per cycle rather than once per member -- at the
     479              : // member declared first -- and the whole ring is named, because "`a` is
     480              : // circular" without the rest of it is not a repair.
     481          117 : static void check_fixity_cycles(R *r) {
     482          117 :   const Fix *f = &r->fix;
     483          173 :   for (u32 i = 0; i < f->n; i++) {
     484           58 :     if (!fix_bit(f, i, i)) continue;
     485              :     bool first = true;
     486            7 :     for (u32 j = 0; j < i; j++)
     487            3 :       if (fix_bit(f, j, j) && fix_bit(f, i, j) && fix_bit(f, j, i)) first = false;
     488            4 :     if (!first) continue;
     489              :     // The ring is listed in declaration order, not walked as a path: the
     490              :     // closure knows WHICH operators are mutually reachable and no longer
     491              :     // knows by what route, and a made-up route would be a worse answer than
     492              :     // the set.
     493              :     char ring[256];
     494              :     usize at = 0;
     495              :     u32 members = 0;
     496            6 :     for (u32 j = 0; j < f->n && at + 2 < sizeof ring; j++) {
     497            4 :       if (j != i && !(fix_bit(f, i, j) && fix_bit(f, j, i))) continue;
     498            4 :       members++;
     499            4 :       int put = snprintf(ring + at, sizeof ring - at, "%s`%.*s`",
     500            4 :                          at == 0 ? "" : ", ", SPAN(r, f->ops[j].name));
     501            4 :       if (put < 0) break;
     502            4 :       at += (usize)put;
     503              :     }
     504            2 :     if (members <= 1)
     505            1 :       diag_fixity(r, f->ops[i].name.off, f->ops[i].name.len,
     506              :                   "`%.*s` is declared tighter than itself",
     507            1 :                   SPAN(r, f->ops[i].name));
     508              :     else
     509            1 :       diag_fixity(r, f->ops[i].name.off, f->ops[i].name.len,
     510              :                   "the fixity order is circular through %s, so no operator in "
     511              :                   "it is loosest",
     512              :                   ring);
     513              :   }
     514          117 : }
     515              : 
     516           75 : static const Op *find_op(const R *r, const Eff *e, WokSpan name) {
     517           85 :   for (u32 i = 0; i < e->nops; i++)
     518           84 :     if (span_eq(r, e->ops[i].name, name)) return &e->ops[i];
     519              :   return nullptr;
     520              : }
     521              : 
     522              : // ------------------------------------------------------------ clause arities
     523              : //
     524              : // The count compared is the PATTERN count, one per argument position before
     525              : // any destructuring, for every clause kind alike -- that is what C8's second
     526              : // amendment buys, and it is why the continuation lives outside `pats`.
     527              : 
     528            4 : static const char *kind_word(u64 kind) {
     529            4 :   if (kind == WOK_CLAUSE_CONTROL) return "control";
     530            2 :   if (kind == WOK_CLAUSE_ABORT) return "abort";
     531              :   return "plain";
     532              : }
     533              : 
     534           72 : static void check_arity(const R *r, const WokNode *clause, const Op *op) {
     535           72 :   u64 kind = H_Clause_kind(clause);
     536           72 :   WokSpan name = H_Clause_name(clause);
     537           72 :   u32 got = H_Clause_pats(clause).n;
     538           72 :   if (got == op->arity) return;
     539              : 
     540              :   // A count of one gets a singular noun. It is one conditional, and a
     541              :   // message that says "binds 1 names" reads as a tool that was not finished.
     542            7 :   const char *plural = op->arity == 1 ? "" : "s";
     543            7 :   const char *bound = got == 1 ? "" : "s";
     544            7 :   if (kind == WOK_CLAUSE_ABORT) {
     545              :     // The mechanical mis-migration: the keyword swapped to `abort` and the v1
     546              :     // continuation binder kept. Naming what abort does NOT bind is the whole
     547              :     // repair, so it is in the message rather than a hint (reject/12).
     548            2 :     wok_diag_add(r->d, WOK_E_ARITY, name.off, name.len,
     549              :                  "`abort %.*s` binds %u name%s but needs %u (%u argument%s; "
     550              :                  "abort binds no continuation)",
     551            2 :                  SPAN(r, name), got, bound, op->arity, op->arity, plural);
     552            2 :     return;
     553              :   }
     554            5 :   if (kind == WOK_CLAUSE_CONTROL) {
     555            1 :     wok_diag_add(r->d, WOK_E_ARITY, name.off, name.len,
     556              :                  "control clause `%.*s` binds %u name%s before the `,` but "
     557              :                  "the op takes %u argument%s; the continuation is the name "
     558              :                  "after it",
     559            1 :                  SPAN(r, name), got, bound, op->arity, plural);
     560            1 :     return;
     561              :   }
     562              :   // A comma-less head is a plain clause, full stop (D25) -- so a v1 control
     563              :   // arm arrives here as a plain one binding a name too many, and the hint is
     564              :   // the edit that makes it the control clause it was meant to be.
     565            4 :   if (got == op->arity + 1)
     566            3 :     wok_diag_add(r->d, WOK_E_ARITY, name.off, name.len,
     567              :                  "plain clause `%.*s` binds %u name%s but the op takes %u "
     568              :                  "argument%s; for the control reading write `,` before the "
     569              :                  "last one",
     570            3 :                  SPAN(r, name), got, bound, op->arity, plural);
     571              :   else
     572            1 :     wok_diag_add(r->d, WOK_E_ARITY, name.off, name.len,
     573              :                  "plain clause `%.*s` binds %u name%s but the op takes %u "
     574              :                  "argument%s",
     575            1 :                  SPAN(r, name), got, bound, op->arity, plural);
     576              : }
     577              : 
     578              : // Clauses for ONE op may be several, with refutable patterns, and they
     579              : // desugar to one clause plus a `case` (D14). That merge is only meaningful if
     580              : // they agree on what the arm DOES: plain arms auto-resume, control and abort
     581              : // arms do not, and no desugaring reconciles the two.
     582           60 : static void check_kind_agreement(const R *r, const WokNode *handler) {
     583           60 :   WokSeq cs = E_Handler_clauses(handler);
     584          234 :   for (u32 i = 0; i < cs.n; i++) {
     585          174 :     const WokNode *a = cs.items[i];
     586          174 :     if (a->tag != H_Clause) continue;
     587          174 :     u64 ka = H_Clause_kind(a);
     588          174 :     if (ka == WOK_CLAUSE_VAR || ka == WOK_CLAUSE_RETURN) continue;
     589              :     // Only the FIRST clause of an op speaks for it: one op, one fault, not
     590              :     // one per later clause of the other kind.
     591              :     bool first = true;
     592          117 :     for (u32 h = 0; h < i && first; h++) {
     593           42 :       const WokNode *e = cs.items[h];
     594           42 :       if (e->tag != H_Clause) continue;
     595           42 :       u64 ke = H_Clause_kind(e);
     596           42 :       if (ke == WOK_CLAUSE_VAR || ke == WOK_CLAUSE_RETURN) continue;
     597           15 :       if (span_eq(r, H_Clause_name(e), H_Clause_name(a))) first = false;
     598              :     }
     599           75 :     if (!first) continue;
     600          169 :     for (u32 j = i + 1; j < cs.n; j++) {
     601          102 :       const WokNode *b = cs.items[j];
     602          202 :       if (b->tag != H_Clause) continue;
     603          102 :       u64 kb = H_Clause_kind(b);
     604          102 :       if (kb == WOK_CLAUSE_VAR || kb == WOK_CLAUSE_RETURN) continue;
     605           14 :       if (!span_eq(r, H_Clause_name(a), H_Clause_name(b))) continue;
     606            5 :       bool plain_a = ka == WOK_CLAUSE_PLAIN;
     607            5 :       bool plain_b = kb == WOK_CLAUSE_PLAIN;
     608            5 :       if (plain_a == plain_b) continue;
     609            2 :       WokSpan at = H_Clause_name(b);
     610            2 :       wok_diag_add(r->d, WOK_E_ARITY, at.off, at.len,
     611              :                    "the clauses for `%.*s` disagree: this one is %s and an "
     612              :                    "earlier one is %s -- they merge into one arm, so they "
     613              :                    "cannot resume differently",
     614            2 :                    SPAN(r, at), kind_word(kb), kind_word(ka));
     615            2 :       break;
     616              :     }
     617              :   }
     618           60 : }
     619              : 
     620           61 : static void check_handler(const R *r, const WokNode *handler) {
     621           61 :   const Eff *e = find_effect(r, E_Handler_effect(handler));
     622              :   // An UNDECLARED effect, and an op the effect does not declare, are both
     623              :   // name-resolution faults that spec.md's registry has no code for. Reporting
     624              :   // them under a code invented here would put a spelling into machine-readable
     625              :   // output that the registry would then have to adopt or break, so the checks
     626              :   // wait on that decision rather than guessing (see the slice spec, section 2).
     627           61 :   if (!e) return;
     628           60 :   check_kind_agreement(r, handler);
     629           60 :   WokSeq cs = E_Handler_clauses(handler);
     630          234 :   for (u32 i = 0; i < cs.n; i++) {
     631          174 :     const WokNode *c = cs.items[i];
     632          174 :     if (c->tag != H_Clause) continue;
     633          174 :     u64 kind = H_Clause_kind(c);
     634          174 :     if (kind == WOK_CLAUSE_VAR || kind == WOK_CLAUSE_RETURN) continue;
     635           75 :     const Op *op = find_op(r, e, H_Clause_name(c));
     636           75 :     if (op && op->known) check_arity(r, c, op);
     637              :   }
     638              : }
     639              : 
     640              : // -------------------------------------------------------- fixity, at the use
     641              : //
     642              : // THE SHIELD RULE. A chain means what the split rule says: find the loosest
     643              : // operator, split there, recurse -- well-formed iff every subchain visited
     644              : // has a unique loosest. That recursive condition collapses into a local one:
     645              : // an incomparable pair of operator OCCURRENCES is licensed iff some
     646              : // occurrence positionally BETWEEN them is strictly looser than both (a
     647              : // SHIELD -- the split there separates the pair before they ever compete).
     648              : // So `2 * 3 + 8 / 4` is fine with `*` and `/` unrelated, and `2 * 3 / 4 + 8`
     649              : // is not: same operators, and the position is the difference. Per occurrence
     650              : // pair, not per distinct pair -- in `a * b + c * d / e` only the first `*`
     651              : // is shielded, and the split genuinely gets stuck on the right segment.
     652              : //
     653              : // PARTIAL KNOWLEDGE. An operator with no entry is SKIPPED, not reported:
     654              : // this is a one-file tool, `fixity` lives in the module that defines the
     655              : // operator, and `+` comes from Base. The same honesty licenses an UNKNOWN
     656              : // occurrence as a shield -- it may be looser than both, and a check that can
     657              : // be wrong is worse than no check. Both graces expire the day an external
     658              : // table arrives with the imports (F5).
     659           82 : static void check_chain(R *r, const WokNode *chain) {
     660           82 :   WokSeq ops = E_Chain_ops(chain);
     661           87 :   if (ops.n < 2 || r->fix.n == 0) return;
     662           49 :   for (u32 i = 0; i < ops.n; i++) {
     663           36 :     u32 a = fix_find(r, H_ChainOp_op(ops.items[i]));
     664           36 :     if (a == UINT32_MAX) continue;
     665           55 :     for (u32 j = i + 1; j < ops.n; j++) {
     666           27 :       u32 b = fix_find(r, H_ChainOp_op(ops.items[j]));
     667           43 :       if (b == UINT32_MAX || a == b) continue;
     668           21 :       if (fix_bit(&r->fix, a, b) || fix_bit(&r->fix, b, a)) continue;
     669              :       bool shielded = false;
     670           13 :       for (u32 k = i + 1; k < j && !shielded; k++) {
     671            4 :         u32 c = fix_find(r, H_ChainOp_op(ops.items[k]));
     672            7 :         shielded = c == UINT32_MAX ||
     673            3 :                    (fix_bit(&r->fix, a, c) && fix_bit(&r->fix, b, c));
     674              :       }
     675            9 :       if (shielded) continue;
     676              :       // Both OCCURRENCES are located, not just the one the span points at:
     677              :       // in a long chain the other end is the thing the reader hunts for.
     678            5 :       WokSpan fst = H_ChainOp_op(ops.items[i]);
     679            5 :       WokSpan at = H_ChainOp_op(ops.items[j]);
     680            5 :       u32 fl = 0, fc = 0;
     681            5 :       wok_diag_position(r->d, fst.off, &fl, &fc);
     682            5 :       u32 sl = 0, sc = 0;
     683            5 :       wok_diag_position(r->d, at.off, &sl, &sc);
     684            5 :       diag_fixity(r, at.off, at.len,
     685              :                   "`%.*s` (%u:%u) and `%.*s` (%u:%u) have no declared order "
     686              :                   "and nothing looser stands between them: bracket the "
     687              :                   "chain, or relate them with `tighter than`",
     688            5 :                   SPAN(r, fst), fl, fc, SPAN(r, at), sl, sc);
     689            5 :       return;  // one chain, one fault: the rest of it says the same thing
     690              :     }
     691              :   }
     692              : }
     693              : 
     694              : // --------------------------------------------------------- write-locality
     695              : //
     696              : // D27: `:=` is not an assignment operator. It is a handler's private write to
     697              : // its own activation frame, and the scope rule is what licenses its absence
     698              : // from effect rows. The target must be a `var` of the handler whose CLAUSE
     699              : // BODY is the write's nearest enclosing function-forming construct.
     700              : //
     701              : // FUNCTION-FORMING, and therefore a boundary: a lambda, a local function
     702              : // equation, a handler literal. NOT boundaries: blocks, `case` arms, `if`
     703              : // branches -- they form no function, so a write inside one is still in the
     704              : // clause body it was written in. A handler literal is a boundary because
     705              : // handler values are first class (C9): one capturing an outer baton's write
     706              : // would be an escaping mutable reference.
     707              : //
     708              : // Clause-body resolution order is args -> batons -> enclosing (D27,
     709              : // normative), and the environment is a linked list on the C stack, so that
     710              : // order is just the order things are pushed.
     711              : 
     712              : typedef struct Scope Scope;
     713              : struct Scope {
     714              :   const Scope *up;
     715              :   WokSpan name;  // the binding site; its .off is quoted when a diagnostic
     716              :                  // names where something was bound
     717              :   bool baton;
     718              :   u32 frame;  // the handler activation a baton belongs to; 0 for a value
     719              : };
     720              : 
     721              : typedef struct {
     722              :   R *r;  // mutable: check_chain caches the fixity note on first fault
     723              :   const Scope *env;
     724              :   const Scope *init_batons;  // when inside a `var` INITIALISER: the frame's
     725              :                              // batons, visible for DIAGNOSIS only -- an
     726              :                              // initialiser runs where the handler value is
     727              :                              // built, before the frame exists, so they are
     728              :                              // deliberately not in `env`
     729              :   u32 frame;   // the frame whose clause body we are DIRECTLY in; 0 = none
     730              :   u32 nframe;  // frames seen, so each handler literal gets its own identity
     731              : } SC;
     732              : 
     733           27 : static const Scope *lookup(const SC *c, WokSpan name) {
     734           51 :   for (const Scope *s = c->env; s; s = s->up)
     735           48 :     if (span_eq(c->r, s->name, name)) return s;
     736              :   return nullptr;
     737              : }
     738              : 
     739              : // The baton this name would have found if nothing shadowed it. Only asked
     740              : // once a write has already failed, to tell "there is no such slot" from "you
     741              : // shadowed the slot", which are different mistakes with different repairs.
     742            8 : static const Scope *lookup_baton(const SC *c, WokSpan name) {
     743           24 :   for (const Scope *s = c->env; s; s = s->up)
     744           20 :     if (s->baton && span_eq(c->r, s->name, name)) return s;
     745              :   return nullptr;
     746              : }
     747              : 
     748              : static void sc_node(SC *c, const WokNode *n);
     749              : 
     750              : // A VALUE binding pushed from a construct the generic walk cannot see:
     751              : // `handle` labels and `use ... as` names are NAME fields, not patterns, so
     752              : // without this a capability that shadows a `var` baton is invisible and a
     753              : // later write to the name is blessed against the baton -- the exact idiom
     754              : // the E-VARSCOPE repair message recommends (`handle s = state 0 ... s.set`).
     755              : // Arena-allocated because the binding outlives the pushing frame.
     756           22 : static const Scope *push_value(SC *c, WokSpan name) {
     757           22 :   Scope *s = WOK_NEW(c->r->arena, Scope);
     758           22 :   *s = (Scope){.up = c->env, .name = name, .baton = false, .frame = 0};
     759           22 :   return s;
     760              : }
     761              : 
     762              : // Every variable a pattern binds, in one pass. Constructors, records and `as`
     763              : // all just carry sub-patterns, so the generic walk finds them.
     764              : //
     765              : // A binder past the cap is REPORTED, not silently dropped: a dropped binder
     766              : // makes a later write look unbound (or un-shadowed), and a wrong answer is
     767              : // worse than an admitted limit. Reported once per store -- the count parks
     768              : // at cap+1 as the already-said marker.
     769          354 : static void bind_pattern(SC *c, const WokNode *p, Scope *store, u32 *nstore,
     770              :                          u32 cap) {
     771          354 :   if (!p || *nstore > cap) return;
     772          354 :   if (p->tag == P_Var || p->tag == P_As) {
     773          269 :     WokSpan name = p->tag == P_Var ? P_Var_name(p) : P_As_name(p);
     774          269 :     if (*nstore == cap) {
     775            1 :       wok_diag_add(c->r->d, WOK_E_DEPTH, name.off, name.len,
     776              :                    "more than %u names bound in one scope; the front end "
     777              :                    "tracks only that many, so give some their own function",
     778              :                    cap);
     779            1 :       *nstore = cap + 1;
     780            1 :       return;
     781              :     }
     782          268 :     Scope *s = &store[(*nstore)++];
     783          268 :     *s = (Scope){.up = c->env, .name = name, .baton = false, .frame = 0};
     784          268 :     c->env = s;
     785          268 :     if (p->tag == P_As) bind_pattern(c, P_As_pat(p), store, nstore, cap);
     786          268 :     return;
     787              :   }
     788           85 :   const WokNodeDesc *d = &wok_node_desc[p->tag];
     789          210 :   for (u16 i = 0; i < d->nfields; i++) {
     790          125 :     if (d->fields[i].family != WFAM_PAT && d->fields[i].family != WFAM_FIELDPAT)
     791           60 :       continue;
     792           65 :     switch (d->fields[i].cls) {
     793           18 :       case WFC_NODE:
     794              :       case WFC_OPT:
     795           18 :         bind_pattern(c, p->slot[i].node, store, nstore, cap);
     796           18 :         break;
     797           47 :       case WFC_SEQ: {
     798           47 :         WokSeq q = wok_seq_unpack(p->slot[i].seq);
     799          150 :         for (u32 j = 0; j < q.n; j++)
     800          103 :           bind_pattern(c, q.items[j], store, nstore, cap);
     801              :         break;
     802              :       }
     803              :       case WFC_NAME:
     804              :       case WFC_TEXT:
     805              :       case WFC_INT:
     806              :       case WFC_FLAG:
     807              :       case WOK_FIELD_CLASS_COUNT:
     808              :         break;
     809              :     }
     810              :   }
     811              : }
     812              : 
     813              : // A pattern binds at most this many names before the walk stops recording
     814              : // them. A binder past the cap makes a write look unbound, so the cap is set
     815              : // far above any pattern a person writes rather than at a plausible number.
     816              : enum { SC_MAX_BINDERS = 64 };
     817              : 
     818           28 : static void check_assign(SC *c, const WokNode *n) {
     819           28 :   const WokNode *target = E_Assign_target(n);
     820           28 :   sc_node(c, E_Assign_value(n));
     821           28 :   if (!target || target->tag != E_Var) {
     822              :     // `f x := e`, `(a, b) := e`: nothing that names a slot. The pyramid's
     823              :     // first line -- `=` names a value, forever -- is the repair. The span is
     824              :     // the TARGET's own, and its subtree is still walked: a write buried in a
     825              :     // malformed target (`g (\y -> s := y) := 1`) is a fault in any position,
     826              :     // and skipping it because its parent is also wrong would hide it.
     827            1 :     if (target)
     828            1 :       wok_diag_add(c->r->d, WOK_E_VARSCOPE, target->off, target->len,
     829              :                    "the target of `:=` must be a `var` baton's name");
     830              :     else
     831            0 :       wok_diag_add(c->r->d, WOK_E_VARSCOPE, n->off, n->len,
     832              :                    "the target of `:=` must be a `var` baton's name");
     833            1 :     sc_node(c, target);
     834           27 :     return;
     835              :   }
     836           27 :   WokSpan name = E_Var_name(target);
     837           27 :   const Scope *found = lookup(c, name);
     838              : 
     839           27 :   if (found && found->baton && found->frame == c->frame && c->frame != 0)
     840              :     return;  // the write is in the clause body of the handler that declares it
     841              : 
     842           14 :   u32 line = 0, col = 0;
     843           14 :   if (found && !found->baton) {
     844            8 :     const Scope *baton = lookup_baton(c, name);
     845            8 :     if (baton && baton->frame == c->frame && c->frame != 0) {
     846              :       // D26 composed with D27: `let cur = cur + 1` read the SLOT and shadowed
     847              :       // it with an arm-local value, so the write now targets the value. The
     848              :       // shadow site is the whole diagnostic -- without it the reader sees a
     849              :       // var they can point at and a message saying it is not one. THIS FRAME
     850              :       // only: the shadow voice promises that un-shadowing repairs the write,
     851              :       // and that is true only of the frame the write is directly in.
     852            3 :       wok_diag_position(c->r->d, found->name.off, &line, &col);
     853            3 :       u32 vline = 0, vcol = 0;
     854            3 :       wok_diag_position(c->r->d, baton->name.off, &vline, &vcol);
     855            3 :       wok_diag_add(c->r->d, WOK_E_VARSCOPE, name.off, name.len,
     856              :                    "`%.*s` is a value here, not a slot: the var declared at "
     857              :                    "%u:%u is shadowed by the binding at %u:%u",
     858            3 :                    SPAN(c->r, name), vline, vcol, line, col);
     859            3 :       return;
     860              :     }
     861              :     if (baton) {
     862              :       // A baton exists but in ANOTHER frame: un-shadowing would only turn
     863              :       // this fault into the cross-frame one, so the boundary is the
     864              :       // diagnosis and the shadow is not mentioned.
     865            1 :       wok_diag_position(c->r->d, baton->name.off, &line, &col);
     866            1 :       wok_diag_add(c->r->d, WOK_E_VARSCOPE, name.off, name.len,
     867              :                    "the var `%.*s` (declared %u:%u) " WOK_VOICE_OUTSIDE_FRAME
     868              :                    ": a lambda, a local function or a nested handler stands "
     869              :                    "between them; snapshot the value, or route the write "
     870              :                    "through an op",
     871            1 :                    SPAN(c->r, name), line, col);
     872            1 :       return;
     873              :     }
     874            4 :     wok_diag_position(c->r->d, found->name.off, &line, &col);
     875            4 :     wok_diag_add(c->r->d, WOK_E_VARSCOPE, name.off, name.len,
     876              :                  "`:=` target `%.*s` (bound %u:%u) is not a `var` baton of an "
     877              :                  "enclosing handler; `:=` is handler frame state, not general "
     878              :                  "mutation",
     879            4 :                  SPAN(c->r, name), line, col);
     880            4 :     return;
     881              :   }
     882            3 :   if (found && found->baton) {
     883            3 :     wok_diag_position(c->r->d, found->name.off, &line, &col);
     884            3 :     wok_diag_add(c->r->d, WOK_E_VARSCOPE, name.off, name.len,
     885              :                  "the var `%.*s` (declared %u:%u) " WOK_VOICE_OUTSIDE_FRAME
     886              :                  ": a lambda, a local function or a nested handler stands "
     887              :                  "between them; snapshot the value, or route the write "
     888              :                  "through an op",
     889            3 :                  SPAN(c->r, name), line, col);
     890            3 :     return;
     891              :   }
     892              :   // Not in scope at all -- but a `var` INITIALISER is a special not-in-scope:
     893              :   // the slot exists in the source, just not yet in time. Saying "no var" to
     894              :   // someone pointing at one two lines up is the wrong voice; the timing is
     895              :   // the diagnosis.
     896            4 :   for (const Scope *s = c->init_batons; s; s = s->up) {
     897            2 :     if (!s->baton || !span_eq(c->r, s->name, name)) continue;
     898            1 :     wok_diag_position(c->r->d, s->name.off, &line, &col);
     899            1 :     wok_diag_add(c->r->d, WOK_E_VARSCOPE, name.off, name.len,
     900              :                  "the var `%.*s` (declared %u:%u) " WOK_VOICE_INIT_WRITE
     901              :                  ": initialisers run where the handler value is built, "
     902              :                  "before the frame exists",
     903            1 :                  SPAN(c->r, name), line, col);
     904            1 :     return;
     905              :   }
     906            2 :   wok_diag_add(c->r->d, WOK_E_VARSCOPE, name.off, name.len,
     907              :                "no `var %.*s` is in scope here; mutation in ordinary code goes "
     908              :                "through an effect (`handle s = state 0` ... `s.set x`)",
     909            2 :                SPAN(c->r, name));
     910              : }
     911              : 
     912              : // A handler literal. Its `var` clauses declare the frame's slots, and they
     913              : // are in scope in every OTHER clause's body -- including the ones declared
     914              : // after it, since a frame is one activation and not a sequence of bindings.
     915           61 : static void sc_handler(SC *c, const WokNode *n) {
     916           61 :   check_handler(c->r, n);  // the declaration-side checks: arity, clause kinds
     917           61 :   WokSeq cs = E_Handler_clauses(n);
     918           61 :   SC inner = *c;
     919           61 :   inner.nframe = c->nframe + 1;
     920           61 :   u32 frame = inner.nframe;
     921              : 
     922           61 :   Scope batons[SC_MAX_BINDERS];
     923           61 :   u32 nb = 0;
     924          235 :   for (u32 i = 0; i < cs.n; i++) {
     925          175 :     const WokNode *cl = cs.items[i];
     926          175 :     if (cl->tag != H_Clause || H_Clause_kind(cl) != WOK_CLAUSE_VAR) continue;
     927           87 :     if (nb == SC_MAX_BINDERS) {
     928              :       // A dropped baton makes every write to it look unbound; say the real
     929              :       // reason once instead.
     930            1 :       WokSpan at = H_Clause_name(cl);
     931            1 :       wok_diag_add(c->r->d, WOK_E_DEPTH, at.off, at.len,
     932              :                    "this handler declares more than %u `var` slots; the "
     933              :                    "front end tracks only that many",
     934              :                    (u32)SC_MAX_BINDERS);
     935            1 :       break;
     936              :     }
     937           86 :     Scope *s = &batons[nb++];
     938           86 :     *s = (Scope){.up = inner.env, .name = H_Clause_name(cl), .baton = true,
     939              :                  .frame = frame};
     940           86 :     inner.env = s;
     941              :   }
     942              : 
     943          236 :   for (u32 i = 0; i < cs.n; i++) {
     944          175 :     const WokNode *cl = cs.items[i];
     945          262 :     if (cl->tag != H_Clause) continue;
     946          175 :     SC arm = inner;
     947          175 :     if (H_Clause_kind(cl) == WOK_CLAUSE_VAR) {
     948              :       // The initialiser is evaluated where the handler VALUE is built, not
     949              :       // inside its own frame: a baton cannot be written before it exists.
     950              :       // The frame's batons ride along for DIAGNOSIS only, so a write to one
     951              :       // is refused in the timing voice rather than as "no such var".
     952           87 :       SC init = *c;
     953           87 :       init.nframe = inner.nframe;
     954           87 :       init.init_batons = inner.env;
     955           87 :       sc_node(&init, H_Clause_body(cl));
     956              :       // Fold the counter back like the arm walk does: a handler literal
     957              :       // inside an initialiser must not share a frame id with one in a later
     958              :       // arm.
     959           87 :       if (init.nframe > inner.nframe) inner.nframe = init.nframe;
     960           87 :       continue;
     961              :     }
     962           88 :     arm.frame = frame;
     963           88 :     Scope args[SC_MAX_BINDERS];
     964           88 :     u32 na = 0;
     965           88 :     WokSeq pats = H_Clause_pats(cl);
     966          170 :     for (u32 j = 0; j < pats.n; j++)
     967           82 :       bind_pattern(&arm, pats.items[j], args, &na, SC_MAX_BINDERS);
     968           88 :     Scope k;
     969           88 :     WokSpan kn = H_Clause_k(cl);
     970           88 :     if (kn.len != 0) {
     971           21 :       k = (Scope){.up = arm.env, .name = kn, .baton = false, .frame = 0};
     972           21 :       arm.env = &k;
     973              :     }
     974           88 :     sc_node(&arm, H_Clause_body(cl));
     975           88 :     if (arm.nframe > inner.nframe) inner.nframe = arm.nframe;
     976              :   }
     977           61 :   c->nframe = inner.nframe;
     978           61 : }
     979              : 
     980              : // A binding. D26 decides the two halves: WITH parameters it is a function
     981              : // equation -- recursive, and its body is a function-forming BOUNDARY. WITHOUT
     982              : // them it is a value binding -- non-recursive, so its right-hand side is
     983              : // walked in the enclosing scope, which is what makes `let off = off + 4` the
     984              : // rebind idiom rather than a loop.
     985          104 : static void sc_bind(SC *c, const WokNode *bind, Scope *slot) {
     986          104 :   const WokNode *lhs = H_Bind_lhs(bind);
     987          104 :   const WokNode *body = H_Bind_body(bind);
     988          104 :   bool is_function = lhs && lhs->tag == L_Prefix && L_Prefix_args(lhs).n != 0;
     989              : 
     990          104 :   if (is_function) {
     991            1 :     SC in = *c;
     992            1 :     in.frame = 0;  // a local function equation is a boundary
     993            1 :     Scope self = {.up = in.env, .name = L_Prefix_name(lhs), .baton = false,
     994              :                   .frame = 0};
     995            1 :     in.env = &self;
     996            1 :     Scope args[SC_MAX_BINDERS];
     997            1 :     u32 na = 0;
     998            1 :     WokSeq ps = L_Prefix_args(lhs);
     999            2 :     for (u32 j = 0; j < ps.n; j++)
    1000            1 :       bind_pattern(&in, ps.items[j], args, &na, SC_MAX_BINDERS);
    1001            1 :     sc_node(&in, body);
    1002            1 :     c->nframe = in.nframe;
    1003            1 :     *slot = (Scope){.up = c->env, .name = L_Prefix_name(lhs), .baton = false,
    1004              :                     .frame = 0};
    1005            1 :     c->env = slot;
    1006            1 :     return;
    1007              :   }
    1008              : 
    1009          103 :   sc_node(c, body);  // NON-recursive: the right-hand side reads the outside
    1010          103 :   if (lhs && lhs->tag == L_Prefix) {
    1011           96 :     *slot = (Scope){.up = c->env, .name = L_Prefix_name(lhs), .baton = false,
    1012              :                     .frame = 0};
    1013           96 :     c->env = slot;
    1014            7 :   } else if (lhs) {
    1015              :     // A destructuring binding's names are needed for the REST of the block,
    1016              :     // so the chain the pattern pushed onto this frame is re-homed into the
    1017              :     // arena, which outlives the walk. bind_pattern pushes sequentially, so
    1018              :     // the chain is store[0..n-1] in push order over the pre-bind env.
    1019            7 :     Scope store[SC_MAX_BINDERS];
    1020            7 :     u32 n = 0;
    1021            7 :     const Scope *before = c->env;
    1022            7 :     bind_pattern(c, lhs, store, &n, SC_MAX_BINDERS);
    1023            7 :     if (n == 0) {
    1024            0 :       c->env = before;  // nothing bound: `let _ = e` leaves the block as-is
    1025            0 :       return;
    1026              :     }
    1027            7 :     if (n > SC_MAX_BINDERS) n = SC_MAX_BINDERS;  // saturation marker clamped
    1028            7 :     Scope *live = WOK_NEW_N(c->r->arena, Scope, n);
    1029            7 :     memcpy(live, store, n * sizeof(Scope));
    1030            7 :     live[0].up = before;
    1031           77 :     for (u32 i = 1; i < n; i++) live[i].up = &live[i - 1];
    1032            7 :     c->env = &live[n - 1];
    1033              :   }
    1034              : }
    1035              : 
    1036              : // A block scopes SEQUENTIALLY: a `let` is visible to the statements after it
    1037              : // and to nothing before it. Blocks are not boundaries, so the frame carries
    1038              : // straight through -- which is what lets a clause body be several lines.
    1039           48 : static void sc_block(SC *c, WokSeq stmts) {
    1040           48 :   SC in = *c;
    1041           48 :   Scope slots[SC_MAX_BINDERS];
    1042           48 :   u32 used = 0;
    1043          235 :   for (u32 i = 0; i < stmts.n; i++) {
    1044          187 :     const WokNode *s = stmts.items[i];
    1045          187 :     if (s && s->tag == S_Handle) {
    1046           14 :       sc_node(&in, S_Handle_handler(s));
    1047              :       // A zero-length label is parse-error recovery (the missing label was
    1048              :       // already faulted), not a binding; same guard as E_HandleIn's elision.
    1049           14 :       WokSpan hl = S_Handle_label(s);
    1050           14 :       if (hl.len != 0) in.env = push_value(&in, hl);
    1051           14 :       continue;
    1052              :     }
    1053          173 :     if (s && s->tag == S_Use) {
    1054            1 :       WokSeq bs = S_Use_binds(s);
    1055            2 :       for (u32 j = 0; j < bs.n; j++) {
    1056            1 :         if (bs.items[j]->tag != H_UseBind) continue;
    1057            1 :         WokSpan to = H_UseBind_to(bs.items[j]);
    1058            1 :         if (to.len != 0) in.env = push_value(&in, to);
    1059              :       }
    1060            1 :       continue;
    1061              :     }
    1062          172 :     if (s && s->tag == S_Let) {
    1063          103 :       if (used < SC_MAX_BINDERS) {
    1064          102 :         sc_bind(&in, S_Let_bind(s), &slots[used++]);
    1065          102 :         continue;
    1066              :       }
    1067            1 :       if (used == SC_MAX_BINDERS) {
    1068              :         // An unrecorded let makes later writes to its name resolve past it;
    1069              :         // say the real reason once, then keep walking without binding.
    1070            1 :         wok_diag_add(c->r->d, WOK_E_DEPTH, s->off, s->len,
    1071              :                      "this block has more than %u `let` bindings; the front "
    1072              :                      "end tracks only that many",
    1073              :                      (u32)SC_MAX_BINDERS);
    1074            1 :         used++;
    1075              :       }
    1076              :     }
    1077           70 :     sc_node(&in, s);
    1078              :   }
    1079           48 :   c->nframe = in.nframe;
    1080           48 : }
    1081              : 
    1082         5129 : static void sc_node(SC *c, const WokNode *n) {
    1083         5129 :   if (!n) return;
    1084         5087 :   if (n->tag == E_Chain) check_chain(c->r, n);  // then children, generically
    1085         5087 :   switch (n->tag) {
    1086           28 :     case E_Assign:
    1087           28 :       check_assign(c, n);
    1088           28 :       return;
    1089           61 :     case E_Handler:
    1090           61 :       sc_handler(c, n);
    1091           61 :       return;
    1092           48 :     case E_Block:
    1093           48 :       sc_block(c, E_Block_stmts(n));
    1094           48 :       return;
    1095           11 :     case E_Lambda: {
    1096           11 :       SC in = *c;
    1097           11 :       in.frame = 0;  // a lambda is a boundary
    1098           11 :       Scope store[SC_MAX_BINDERS];
    1099           11 :       u32 nb = 0;
    1100           11 :       WokSeq ps = E_Lambda_params(n);
    1101           23 :       for (u32 j = 0; j < ps.n; j++)
    1102           12 :         bind_pattern(&in, ps.items[j], store, &nb, SC_MAX_BINDERS);
    1103           11 :       sc_node(&in, E_Lambda_body(n));
    1104           11 :       c->nframe = in.nframe;
    1105           11 :       return;
    1106              :     }
    1107            2 :     case E_LetIn: {
    1108            2 :       SC in = *c;
    1109            2 :       Scope slot;
    1110            2 :       sc_bind(&in, E_LetIn_bind(n), &slot);
    1111            2 :       sc_node(&in, E_LetIn_body(n));
    1112            2 :       c->nframe = in.nframe;
    1113            2 :       return;
    1114              :     }
    1115           19 :     case E_HandleIn: {
    1116           19 :       sc_node(c, E_HandleIn_handler(n));  // the handler reads the outside
    1117           19 :       SC in = *c;
    1118           19 :       WokSpan label = E_HandleIn_label(n);
    1119           19 :       if (label.len != 0) in.env = push_value(&in, label);  // empty = elided
    1120           19 :       sc_node(&in, E_HandleIn_body(n));
    1121           19 :       c->nframe = in.nframe;
    1122           19 :       return;
    1123              :     }
    1124            4 :     case E_UseIn: {
    1125            4 :       SC in = *c;
    1126            4 :       WokSeq bs = E_UseIn_binds(n);
    1127            8 :       for (u32 j = 0; j < bs.n; j++) {
    1128            4 :         if (bs.items[j]->tag != H_UseBind) continue;
    1129            4 :         WokSpan to = H_UseBind_to(bs.items[j]);
    1130            4 :         if (to.len != 0) in.env = push_value(&in, to);
    1131              :       }
    1132            4 :       sc_node(&in, E_UseIn_body(n));
    1133            4 :       c->nframe = in.nframe;
    1134            4 :       return;
    1135              :     }
    1136           46 :     case H_Alt: {
    1137           46 :       SC in = *c;
    1138           46 :       Scope store[SC_MAX_BINDERS];
    1139           46 :       u32 nb = 0;
    1140           46 :       bind_pattern(&in, H_Alt_pat(n), store, &nb, SC_MAX_BINDERS);
    1141              :       // A `where` holds local function equations, which are boundaries of
    1142              :       // their own; sc_decl handles that.
    1143           46 :       WokSeq w = H_Alt_wheres(n);
    1144           46 :       for (u32 j = 0; j < w.n; j++) sc_node(&in, w.items[j]);
    1145           46 :       sc_node(&in, H_Alt_body(n));
    1146           46 :       c->nframe = in.nframe;
    1147           46 :       return;
    1148              :     }
    1149          214 :     case D_Equation: {
    1150          214 :       SC in = *c;
    1151          214 :       in.frame = 0;  // a top-level or local equation is a boundary
    1152          214 :       const WokNode *lhs = D_Equation_lhs(n);
    1153          214 :       Scope store[SC_MAX_BINDERS];
    1154          214 :       u32 nb = 0;
    1155          426 :       if (lhs && lhs->tag == L_Prefix) {
    1156          212 :         WokSeq ps = L_Prefix_args(lhs);
    1157          292 :         for (u32 j = 0; j < ps.n; j++)
    1158           80 :           bind_pattern(&in, ps.items[j], store, &nb, SC_MAX_BINDERS);
    1159            2 :       } else if (lhs && lhs->tag == L_Infix) {
    1160            2 :         bind_pattern(&in, L_Infix_left(lhs), store, &nb, SC_MAX_BINDERS);
    1161            2 :         bind_pattern(&in, L_Infix_right(lhs), store, &nb, SC_MAX_BINDERS);
    1162              :       }
    1163          214 :       WokSeq w = D_Equation_wheres(n);
    1164          217 :       for (u32 j = 0; j < w.n; j++) sc_node(&in, w.items[j]);
    1165          214 :       sc_node(&in, D_Equation_body(n));
    1166          214 :       c->nframe = in.nframe;
    1167          214 :       return;
    1168              :     }
    1169              :     default:
    1170         4654 :       break;
    1171              :   }
    1172              :   // Everything else is TRANSPARENT: it binds nothing and forms no function,
    1173              :   // so the frame and the environment pass straight through. `case` scrutinees,
    1174              :   // `if` branches, applications, tuples, records -- all of them.
    1175         4654 :   const WokNodeDesc *d = &wok_node_desc[n->tag];
    1176        12140 :   for (u16 i = 0; i < d->nfields; i++) {
    1177         7486 :     switch (d->fields[i].cls) {
    1178         2218 :       case WFC_NODE:
    1179              :       case WFC_OPT:
    1180         2218 :         sc_node(c, n->slot[i].node);
    1181         2218 :         break;
    1182         1484 :       case WFC_SEQ: {
    1183         1484 :         WokSeq q = wok_seq_unpack(n->slot[i].seq);
    1184         3568 :         for (u32 j = 0; j < q.n; j++) sc_node(c, q.items[j]);
    1185              :         break;
    1186              :       }
    1187              :       case WFC_NAME:
    1188              :       case WFC_TEXT:
    1189              :       case WFC_INT:
    1190              :       case WFC_FLAG:
    1191              :       case WOK_FIELD_CLASS_COUNT:
    1192              :         break;
    1193              :     }
    1194              :   }
    1195              : }
    1196              : 
    1197          117 : void wok_resolve_fix(const WokNode *file, const char *src, WokArena *a,
    1198              :                      WokDiagSink *d, WokFixTable *out_fix) {
    1199          117 :   if (!file || file->tag != W_File) {
    1200            0 :     if (out_fix) *out_fix = (WokFixTable){0};
    1201            0 :     return;
    1202              :   }
    1203          117 :   R r = {.src = src, .d = d, .file = file, .arena = a, .effs = nullptr,
    1204              :          .neffs = 0, .fix = {0}, .fix_note = nullptr};
    1205          117 :   build_aliases(&r, file, a);
    1206          117 :   build_table(&r, file, a);
    1207          117 :   build_fixity(&r, file, a);
    1208          117 :   check_fixity_cycles(&r);
    1209              :   // ONE traversal: the sc walk visits every node (its default arm recurses
    1210              :   // generically), so the chain and handler checks ride it instead of paying
    1211              :   // a second full pass. Diagnostics therefore arrive in TREE order, not
    1212              :   // check-kind order.
    1213          117 :   SC sc = {.r = &r, .env = nullptr, .frame = 0, .nframe = 0};
    1214          117 :   sc_node(&sc, (WokNode *)file);
    1215          117 :   if (out_fix)
    1216           13 :     *out_fix = (WokFixTable){.ops = r.fix.ops, .n = r.fix.n,
    1217           13 :                              .edge = r.fix.edge, .words = r.fix.words};
    1218              : }
    1219              : 
    1220          104 : void wok_resolve(const WokNode *file, const char *src, WokArena *a,
    1221              :                  WokDiagSink *d) {
    1222          104 :   wok_resolve_fix(file, src, a, d, nullptr);
    1223          104 : }
        

Generated by: LCOV version 2.4-1