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 : }
|