@bitspark/horos 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,17 +19,18 @@
19
19
  * - union/and are KLEENE: a DECISIVE alternative beats an exhausted sibling, so the evaluator
20
20
  * short-circuits on a decisive result but NEVER on an Exhausted. The answer is a function of
21
21
  * the SET of sub-outcomes, never of traversal order.
22
+ *
23
+ * Every outcome other than `conforms` carries one LOCATION (ADR 0009, docs/spec/horos.md
24
+ * "Diagnostic locations"): the value path, the descriptor position and a reason, chosen in a
25
+ * fixed order so the Go, Rust and TypeScript cores report the identical location. A location
26
+ * never changes which verdict is reported.
22
27
  */
23
- import { readList, readMap, readSet, readText } from "@bitspark/ontos-data";
28
+ import { readInt, readList, readMap, readSet, readText } from "@bitspark/ontos-data";
24
29
  import { ATOM_LEN_BOUND_MAX, ATOM_LEN_BOUND_MIN, isLeafCtor, leafClassifies, leafWellFormed, MAX_RECURSION_DEPTH, NAMESPACE, readFieldDescriptor, } from "./core.js";
25
30
  /** The effective value of a limit in this build. */
26
31
  export function limitValue(l) {
27
32
  return l === "recursion_depth" ? MAX_RECURSION_DEPTH : 0;
28
33
  }
29
- const CONFORMS = { verdict: "conforms" };
30
- const MISMATCH = { verdict: "mismatch" };
31
- const NOT_WF = { verdict: "not_well_formed" };
32
- const exhausted = (limit) => ({ verdict: "exhausted", limit });
33
34
  /** Whether this outcome is a completed decision. */
34
35
  export function isComplete(d) {
35
36
  return d.verdict !== "exhausted";
@@ -59,15 +60,60 @@ export function checkingProfile() {
59
60
  atomLenBoundMax: ATOM_LEN_BOUND_MAX,
60
61
  };
61
62
  }
62
- // ----- the Kleene folds (ADR 0008 D2/D3) -----
63
+ function stepInto(path, step) {
64
+ return { parent: path, step, length: pathLength(path) + 1 };
65
+ }
66
+ function pathLength(path) {
67
+ return path === undefined ? 0 : path.length;
68
+ }
69
+ function at(pos, index) {
70
+ return { parent: pos, index };
71
+ }
72
+ const CONFORMS = { verdict: "conforms" };
73
+ const CONFORMS_D = { verdict: "conforms" };
74
+ const mismatch = (fault) => ({ verdict: "mismatch", fault });
75
+ const notWf = (fault) => ({ verdict: "not_well_formed", fault });
76
+ const exhausted = (path, pos) => ({
77
+ verdict: "exhausted",
78
+ limit: "recursion_depth",
79
+ fault: { path, at: pos, reason: "limit" },
80
+ });
81
+ function materialize(f) {
82
+ const path = [];
83
+ for (let n = f.path; n !== undefined; n = n.parent)
84
+ path.push(n.step);
85
+ path.reverse();
86
+ const pos = [];
87
+ for (let n = f.at; n !== undefined; n = n.parent)
88
+ pos.push(n.index);
89
+ pos.reverse();
90
+ if (f.field !== undefined)
91
+ return { path, at: pos, reason: f.reason, field: f.field };
92
+ if (f.arg !== undefined)
93
+ return { path, at: pos, reason: f.reason, arg: f.arg };
94
+ return { path, at: pos, reason: f.reason };
95
+ }
96
+ function report(o) {
97
+ switch (o.verdict) {
98
+ case "conforms":
99
+ return CONFORMS_D;
100
+ case "mismatch":
101
+ return { verdict: "mismatch", location: materialize(o.fault) };
102
+ case "not_well_formed":
103
+ return { verdict: "not_well_formed", location: materialize(o.fault) };
104
+ case "exhausted":
105
+ return { verdict: "exhausted", limit: o.limit, location: materialize(o.fault) };
106
+ }
107
+ }
108
+ // ----- the Kleene folds (ADR 0008 D2/D3), with the location order of ADR 0009 -----
63
109
  /**
64
110
  * `not`: swap the completed verdicts, PRESERVE exhaustion. Never completes an incomplete
65
- * subdecision — the rule this module exists for.
111
+ * subdecision — the rule this module exists for. A `not` that fails reports itself.
66
112
  */
67
- function kleeneNot(inner) {
113
+ function kleeneNot(inner, path, pos) {
68
114
  switch (inner.verdict) {
69
115
  case "conforms":
70
- return MISMATCH;
116
+ return mismatch({ path, at: pos, reason: "complement" });
71
117
  case "mismatch":
72
118
  return CONFORMS;
73
119
  default:
@@ -75,60 +121,75 @@ function kleeneNot(inner) {
75
121
  return inner;
76
122
  }
77
123
  }
78
- /** Kleene disjunction: a decisive `conforms` wins over an exhausted sibling. */
79
- function kleeneAny(outcomes) {
80
- let limit;
81
- let notWF = false;
82
- for (const o of outcomes) {
83
- if (o.verdict === "conforms")
84
- return CONFORMS; // decisive — stop here
85
- if (o.verdict === "not_well_formed")
86
- notWF = true;
87
- else if (o.verdict === "exhausted" && limit === undefined)
88
- limit = o.limit;
124
+ /**
125
+ * Kleene conjunction over `count` sub-outcomes in the fixed order: a decisive `mismatch`
126
+ * wins over an exhausted sibling and is the first one in order. Otherwise the first
127
+ * exhausted, then the first not_well_formed sub-outcome decides.
128
+ */
129
+ function kleeneAll(count, outcome) {
130
+ let firstExhausted;
131
+ let firstNotWf;
132
+ for (let i = 0; i < count; i += 1) {
133
+ const o = outcome(i);
134
+ if (o.verdict === "mismatch")
135
+ return o; // decisive — stop here
136
+ if (o.verdict === "exhausted")
137
+ firstExhausted ??= o;
138
+ else if (o.verdict === "not_well_formed")
139
+ firstNotWf ??= o;
89
140
  }
90
- if (limit !== undefined)
91
- return exhausted(limit);
92
- return notWF ? NOT_WF : MISMATCH;
141
+ return firstExhausted ?? firstNotWf ?? CONFORMS;
93
142
  }
94
- /** Kleene conjunction: a decisive `mismatch` wins over an exhausted sibling. */
95
- function kleeneAll(outcomes) {
96
- let limit;
97
- let notWF = false;
98
- for (const o of outcomes) {
99
- if (o.verdict === "mismatch")
100
- return MISMATCH; // decisive — stop here
101
- if (o.verdict === "not_well_formed")
102
- notWF = true;
103
- else if (o.verdict === "exhausted" && limit === undefined)
104
- limit = o.limit;
143
+ /**
144
+ * Kleene disjunction for `union`: a decisive `conforms` wins over an exhausted sibling. A
145
+ * union with no conforming alternative is exhausted at its first exhausted alternative. A
146
+ * mismatching union reports the first alternative with the longest value path when that
147
+ * path goes deeper than the union's own value, and otherwise reports itself.
148
+ */
149
+ function kleeneAny(count, outcome, path, pos) {
150
+ let firstExhausted;
151
+ let firstNotWf;
152
+ let deepest;
153
+ let deepestLength = pathLength(path);
154
+ for (let i = 0; i < count; i += 1) {
155
+ const o = outcome(i);
156
+ if (o.verdict === "conforms")
157
+ return CONFORMS; // decisive — stop here
158
+ if (o.verdict === "exhausted")
159
+ firstExhausted ??= o;
160
+ else if (o.verdict === "not_well_formed")
161
+ firstNotWf ??= o;
162
+ else if (pathLength(o.fault.path) > deepestLength) {
163
+ deepest = o;
164
+ deepestLength = pathLength(o.fault.path);
165
+ }
105
166
  }
106
- if (limit !== undefined)
107
- return exhausted(limit);
108
- return notWF ? NOT_WF : CONFORMS;
167
+ return (firstExhausted ??
168
+ firstNotWf ??
169
+ deepest ??
170
+ mismatch({ path, at: pos, reason: "no-alternative" }));
109
171
  }
110
172
  // ----- the public entry points -----
111
173
  /**
112
174
  * Diagnose `wf(T)`: `conforms` when T is a well-formed descriptor, `not_well_formed` when it is
113
175
  * conclusively not, and `exhausted` when the well-formedness traversal itself could not finish
114
- * — the distinction horos-internal#211 asks for by name.
176
+ * — the distinction horos-internal#211 asks for by name. A non-conforming outcome names the
177
+ * descriptor position and, where one argument is at fault, its item index.
115
178
  */
116
179
  export function diagnoseWf(t) {
117
- return diagnoseWfIn(t, [], [], 0);
180
+ return report(diagnoseWfIn(t, [], [], 0, undefined));
118
181
  }
119
182
  /**
120
183
  * Diagnose `v : T`. Mirrors `conforms`: the `wf(T)` soundness gate runs first (spec §3 MUST),
121
184
  * so a descriptor that is not well-formed yields `not_well_formed` and one whose
122
185
  * well-formedness could not be decided yields `exhausted` — neither is reported as a mismatch
123
- * of the value.
186
+ * of the value. Each carries the well-formedness location, with an empty value path.
124
187
  */
125
188
  export function diagnose(v, t) {
126
- const w = diagnoseWf(t);
127
- if (w.verdict === "conforms")
128
- return diagnoseIn(v, t, [], [], 0);
129
- if (w.verdict === "exhausted")
130
- return w;
131
- return NOT_WF;
189
+ const w = diagnoseWfIn(t, [], [], 0, undefined);
190
+ if (w.verdict !== "conforms")
191
+ return report(w);
192
+ return report(diagnoseIn(v, t, [], 0, undefined, undefined));
132
193
  }
133
194
  function head(t) {
134
195
  if (t.kind !== "tuple")
@@ -161,239 +222,393 @@ function atomStringOf(v) {
161
222
  key += String.fromCharCode(byte);
162
223
  return key;
163
224
  }
164
- function diagnoseIn(v, t, env, pending, depth) {
225
+ function diagnoseIn(v, t, env, depth, path, pos) {
165
226
  if (depth > MAX_RECURSION_DEPTH)
166
- return exhausted("recursion_depth");
227
+ return exhausted(path, pos);
228
+ const refused = () => mismatch({ path, at: pos, reason: "refused" });
167
229
  const h = head(t);
168
230
  if (h === undefined)
169
- return MISMATCH; // not a horos descriptor
231
+ return refused(); // not a horos descriptor; unreachable past the wf gate
170
232
  const { ctor, args } = h;
171
233
  // Leaves never recurse into a value position, so they are always decisive here.
172
234
  if (isLeafCtor(ctor))
173
- return leafClassifies(v, t) ? CONFORMS : MISMATCH;
235
+ return leafClassifies(v, t) ? CONFORMS : refused();
174
236
  switch (ctor) {
175
- // Composition on the SAME v — pending threads through unchanged.
237
+ // Composition on the SAME v.
176
238
  case "union":
177
- return kleeneAny(args.map((ct) => diagnoseIn(v, ct, env, pending, depth + 1)));
239
+ return kleeneAny(args.length, (i) => diagnoseIn(v, args[i], env, depth + 1, path, at(pos, i + 1)), path, pos);
178
240
  case "and":
179
- return kleeneAll(args.map((ct) => diagnoseIn(v, ct, env, pending, depth + 1)));
241
+ return kleeneAll(args.length, (i) => diagnoseIn(v, args[i], env, depth + 1, path, at(pos, i + 1)));
180
242
  case "not": {
181
243
  if (args.length !== 1)
182
- return MISMATCH;
183
- return kleeneNot(diagnoseIn(v, args[0], env, pending, depth + 1));
244
+ return refused();
245
+ return kleeneNot(diagnoseIn(v, args[0], env, depth + 1, path, at(pos, 1)), path, pos);
184
246
  }
185
- // Recursion — mirrors the recognizer's guard exactly.
247
+ // Recursion — mirrors the recognizer, and is transparent to locations. A ref denotes
248
+ // the rec that lexically encloses it and continues in that binding's environment, one
249
+ // counted frame deeper (ADR 0011). `env` holds exactly the enclosing bindings,
250
+ // outermost first, so a binding's closure is the prefix of `env` that ends with it.
186
251
  case "rec": {
187
252
  if (args.length !== 2)
188
- return MISMATCH;
253
+ return refused();
189
254
  const name = atomStringOf(args[0]);
190
255
  if (name === undefined)
191
- return MISMATCH;
256
+ return refused();
192
257
  const body = args[1];
193
- return diagnoseIn(v, body, [...env, { name, body }], [...pending, name], depth + 1);
258
+ const bodyAt = at(pos, 2);
259
+ return diagnoseIn(v, body, [...env, { name, body, at: bodyAt }], depth + 1, path, bodyAt);
194
260
  }
195
261
  case "ref": {
196
262
  if (args.length !== 1)
197
- return MISMATCH;
263
+ return refused();
198
264
  const name = atomStringOf(args[0]);
199
265
  if (name === undefined)
200
- return MISMATCH;
201
- if (pending.includes(name))
202
- return MISMATCH; // unguarded — the recognizer's false
266
+ return refused();
203
267
  for (let i = env.length - 1; i >= 0; i -= 1) {
204
- if (env[i].name === name) {
205
- return diagnoseIn(v, env[i].body, env, [...pending, name], depth + 1);
268
+ const b = env[i];
269
+ if (b.name === name) {
270
+ return diagnoseIn(v, b.body, env.slice(0, i + 1), depth + 1, path, b.at);
206
271
  }
207
272
  }
208
- return MISMATCH; // unbound
273
+ return refused(); // unbound
209
274
  }
210
275
  }
211
- // Structural arms: the gate is decisive, and a descent into a CHILD VALUE clears pending.
212
- const children = structuralChildren(v, ctor, args);
213
- if (children === undefined)
214
- return MISMATCH;
215
- return kleeneAll(children.map(([cv, ct]) => diagnoseIn(cv, ct, env, [], depth + 1)));
276
+ // Structural arms: the gate is decisive, then each child value against its descriptor.
277
+ const gate = structuralChildren(v, ctor, args, path, pos);
278
+ if (!Array.isArray(gate))
279
+ return mismatch(gate);
280
+ const children = gate;
281
+ return kleeneAll(children.length, (i) => {
282
+ const c = children[i];
283
+ return diagnoseIn(c.v, c.t, env, depth + 1, stepInto(path, c.step), c.at);
284
+ });
216
285
  }
217
286
  /**
218
287
  * The structural gate plus the ordered child sub-judgments, for the arms that descend into a
219
- * child VALUE. Mirrors the recognizer's recursive arms; `undefined` means the gate failed, so
220
- * `v` cannot conform regardless of any child.
288
+ * child VALUE. Mirrors the recognizer's recursive arms; a {@link Fault} means the gate failed,
289
+ * so `v` cannot conform regardless of any child.
221
290
  */
222
- function structuralChildren(v, ctor, args) {
291
+ function structuralChildren(v, ctor, args, path, pos) {
292
+ const refused = { path, at: pos, reason: "refused" };
223
293
  switch (ctor) {
224
294
  case "tuple": {
225
295
  if (v.kind !== "tuple")
226
- return undefined;
296
+ return refused;
227
297
  const items = v.items();
228
298
  if (items.length !== args.length)
229
- return undefined;
230
- return items.map((c, i) => [c, args[i]]);
299
+ return refused;
300
+ return items.map((c, i) => ({
301
+ v: c,
302
+ t: args[i],
303
+ step: { kind: "item", index: i },
304
+ at: at(pos, i + 1),
305
+ }));
231
306
  }
232
307
  case "seq": {
233
308
  if (args.length !== 1 || v.kind !== "tuple")
234
- return undefined;
235
- return v.items().map((c) => [c, args[0]]);
309
+ return refused;
310
+ const childAt = at(pos, 1);
311
+ return v.items().map((c, i) => ({
312
+ v: c,
313
+ t: args[0],
314
+ step: { kind: "item", index: i },
315
+ at: childAt,
316
+ }));
236
317
  }
237
318
  case "compound": {
238
319
  if (args.length < 1 || v.kind !== "tuple")
239
- return undefined;
320
+ return refused;
240
321
  const label = atomBytesOf(args[0]);
241
322
  const items = v.items();
242
323
  if (label === undefined || items.length !== args.length)
243
- return undefined;
324
+ return refused;
244
325
  const got = atomBytesOf(items[0]);
245
326
  if (got === undefined ||
246
327
  got.length !== label.length ||
247
328
  got.some((b, i) => b !== label[i]))
248
- return undefined;
249
- return args.slice(1).map((ct, i) => [items[i + 1], ct]);
329
+ return refused;
330
+ // Value item j (j >= 1) is checked by descriptor argument j, at item index j + 1.
331
+ return args.slice(1).map((ct, i) => ({
332
+ v: items[i + 1],
333
+ t: ct,
334
+ step: { kind: "item", index: i + 1 },
335
+ at: at(pos, i + 2),
336
+ }));
250
337
  }
251
338
  case "list-of":
252
339
  case "set-of": {
253
340
  if (args.length !== 1)
254
- return undefined;
341
+ return refused;
342
+ let elems;
255
343
  try {
256
- const elems = ctor === "list-of" ? readList(v) : readSet(v);
257
- return elems.map((e) => [e, args[0]]);
344
+ elems = ctor === "list-of" ? readList(v) : readSet(v);
258
345
  }
259
346
  catch {
260
- return undefined;
347
+ return refused;
261
348
  }
349
+ const childAt = at(pos, 1);
350
+ return elems.map((e, i) => ({
351
+ v: e,
352
+ t: args[0],
353
+ step: { kind: "elem", index: i },
354
+ at: childAt,
355
+ }));
262
356
  }
263
357
  case "map-of": {
264
358
  if (args.length !== 2)
265
- return undefined;
359
+ return refused;
360
+ let entries;
266
361
  try {
267
- const out = [];
268
- for (const [k, val] of readMap(v)) {
269
- out.push([k, args[0]], [val, args[1]]);
270
- }
271
- return out;
362
+ entries = readMap(v);
272
363
  }
273
364
  catch {
274
- return undefined;
365
+ return refused;
275
366
  }
367
+ const keyAt = at(pos, 1);
368
+ const valueAt = at(pos, 2);
369
+ const out = [];
370
+ entries.forEach(([k, val], i) => {
371
+ out.push({ v: k, t: args[0], step: { kind: "key", index: i }, at: keyAt }, { v: val, t: args[1], step: { kind: "value", index: i }, at: valueAt });
372
+ });
373
+ return out;
276
374
  }
277
375
  case "record":
278
- return args.length < 1 ? undefined : recordChildren(v, args[0], args.slice(1));
376
+ return args.length < 1
377
+ ? refused
378
+ : recordChildren(v, args[0], args.slice(1), path, pos);
279
379
  }
280
- return undefined;
380
+ return refused;
281
381
  }
282
- /** Mirrors the recognizer's record gate and present-field worklist. */
283
- function recordChildren(v, mode, fields) {
382
+ /**
383
+ * Mirrors the recognizer's record gate and present-field worklist, in the fixed order of
384
+ * ADR 0009: a canonical map, text keys, missing required fields in descriptor order, extra
385
+ * fields of a closed record in canonical key order, then the present declared fields.
386
+ */
387
+ function recordChildren(v, mode, fields, path, pos) {
388
+ const refused = { path, at: pos, reason: "refused" };
284
389
  const m = atomStringOf(mode);
285
390
  if (m !== "open" && m !== "closed")
286
- return undefined;
287
- const closed = m === "closed";
391
+ return refused;
288
392
  let entries;
289
393
  try {
290
394
  entries = readMap(v);
291
395
  }
292
396
  catch {
293
- return undefined;
397
+ return refused;
294
398
  }
295
399
  const keyed = new Map();
296
- for (const [k, val] of entries) {
400
+ for (let i = 0; i < entries.length; i += 1) {
401
+ const [k, val] = entries[i];
297
402
  try {
298
403
  keyed.set(readText(k), val);
299
404
  }
300
405
  catch {
301
- return undefined;
406
+ return {
407
+ path: stepInto(path, { kind: "key", index: i }),
408
+ at: pos,
409
+ reason: "refused",
410
+ };
302
411
  }
303
412
  }
304
413
  const out = [];
305
414
  const declared = new Set();
306
- for (const f of fields) {
307
- const fd = readFieldDescriptor(f);
415
+ for (let j = 0; j < fields.length; j += 1) {
416
+ const fd = readFieldDescriptor(fields[j]);
308
417
  if (fd === undefined)
309
- return undefined;
418
+ return refused;
310
419
  declared.add(fd.name);
311
420
  const val = keyed.get(fd.name);
312
421
  if (val === undefined) {
313
422
  if (fd.required)
314
- return undefined; // a missing required field is decisive
423
+ return { path, at: pos, reason: "missing-field", field: fd.name };
315
424
  continue;
316
425
  }
317
- out.push([val, fd.ty]);
426
+ // Field j is descriptor item j + 2; its type is item 3 of the field form.
427
+ out.push({
428
+ v: val,
429
+ t: fd.ty,
430
+ step: { kind: "field", name: fd.name },
431
+ at: at(at(pos, j + 2), 3),
432
+ });
318
433
  }
319
- if (closed) {
434
+ if (m === "closed") {
320
435
  for (const k of keyed.keys()) {
321
436
  if (!declared.has(k))
322
- return undefined; // an extra key in a closed record is decisive
437
+ return { path, at: pos, reason: "extra-field", field: k };
323
438
  }
324
439
  }
325
440
  return out;
326
441
  }
327
- function diagnoseWfIn(t, bound, pending, depth) {
442
+ /** In-range check of an `atom-len`/`text-len` bound: the signed 128-bit cap. */
443
+ function boundInRange(n) {
444
+ return n >= ATOM_LEN_BOUND_MIN && n <= ATOM_LEN_BOUND_MAX;
445
+ }
446
+ /**
447
+ * The leaf constructors' own-form check, in the fixed order of ADR 0009: arity, then each
448
+ * argument in item order against the arguments before it. `undefined` is well formed;
449
+ * otherwise the reason and, for an argument, its item index. Mirrors the recognizer's
450
+ * `wfIn` leaf arms, which decide the same set.
451
+ */
452
+ function leafWfFault(ctor, args) {
453
+ const arity = (n) => args.length === n ? undefined : { reason: "arity" };
454
+ const argument = (i) => ({ reason: "argument", arg: i });
455
+ switch (ctor) {
456
+ case "any":
457
+ case "atom":
458
+ case "key-grammar":
459
+ case "text-non-blank":
460
+ case "lowercase-uuid":
461
+ return arity(0);
462
+ case "exact":
463
+ return arity(1);
464
+ case "atom-exact":
465
+ return arity(1) ?? (atomBytesOf(args[0]) === undefined ? argument(1) : undefined);
466
+ case "int-min":
467
+ case "int-max": {
468
+ const a = arity(1);
469
+ if (a !== undefined)
470
+ return a;
471
+ try {
472
+ return boundInRange(readInt(args[0])) ? undefined : argument(1);
473
+ }
474
+ catch {
475
+ return argument(1);
476
+ }
477
+ }
478
+ case "atom-len":
479
+ case "text-len": {
480
+ const a = arity(2);
481
+ if (a !== undefined)
482
+ return a;
483
+ let lo;
484
+ try {
485
+ lo = readInt(args[0]);
486
+ }
487
+ catch {
488
+ return argument(1);
489
+ }
490
+ if (!boundInRange(lo) || lo < 0n)
491
+ return argument(1);
492
+ let hi;
493
+ try {
494
+ hi = readInt(args[1]);
495
+ }
496
+ catch {
497
+ return argument(2);
498
+ }
499
+ return !boundInRange(hi) || hi < lo ? argument(2) : undefined;
500
+ }
501
+ case "atom-in": {
502
+ const a = arity(1);
503
+ if (a !== undefined)
504
+ return a;
505
+ let elems;
506
+ try {
507
+ elems = readSet(args[0]);
508
+ }
509
+ catch {
510
+ return argument(1);
511
+ }
512
+ return elems.every((e) => e.kind === "atom") ? undefined : argument(1);
513
+ }
514
+ case "scalar": {
515
+ const a = arity(1);
516
+ if (a !== undefined)
517
+ return a;
518
+ const kind = atomStringOf(args[0]);
519
+ return kind === "int" || kind === "utf8-text" || kind === "bool"
520
+ ? undefined
521
+ : argument(1);
522
+ }
523
+ }
524
+ return { reason: "unknown-constructor" };
525
+ }
526
+ function diagnoseWfIn(t, bound, pending, depth, pos) {
328
527
  if (depth > MAX_RECURSION_DEPTH)
329
- return exhausted("recursion_depth");
528
+ return exhausted(undefined, pos);
529
+ const fault = (reason, arg) => notWf(arg === undefined
530
+ ? { path: undefined, at: pos, reason }
531
+ : { path: undefined, at: pos, reason, arg });
330
532
  const h = head(t);
331
533
  if (h === undefined)
332
- return NOT_WF;
534
+ return fault("not-descriptor");
333
535
  const { ctor, args } = h;
334
- if (isLeafCtor(ctor))
335
- return leafWellFormed(t) ? CONFORMS : NOT_WF;
336
- const wfAll = (ts, p) => kleeneAll(ts.map((ct) => diagnoseWfIn(ct, bound, p, depth + 1)));
536
+ if (isLeafCtor(ctor)) {
537
+ // The recognizer decides the verdict; leafWfFault only names the fault.
538
+ if (leafWellFormed(t))
539
+ return CONFORMS;
540
+ const f = leafWfFault(ctor, args) ?? { reason: "argument" };
541
+ return fault(f.reason, f.arg);
542
+ }
543
+ // Child descriptors at the given item indices, in order, after the own form is accepted.
544
+ const wfChildren = (items, p) => kleeneAll(items.length, (i) => diagnoseWfIn(args[items[i] - 1], bound, p, depth + 1, at(pos, items[i])));
545
+ const all = (from) => {
546
+ const out = [];
547
+ for (let i = from; i <= args.length; i += 1)
548
+ out.push(i);
549
+ return out;
550
+ };
337
551
  switch (ctor) {
338
552
  case "tuple":
339
- return wfAll(args, []);
553
+ return wfChildren(all(1), []);
340
554
  case "seq":
341
- return args.length !== 1 ? NOT_WF : diagnoseWfIn(args[0], bound, [], depth + 1);
342
- case "compound":
343
- if (args.length < 1 || atomBytesOf(args[0]) === undefined)
344
- return NOT_WF;
345
- return wfAll(args.slice(1), []);
346
555
  case "list-of":
347
556
  case "set-of":
348
- return args.length !== 1 ? NOT_WF : diagnoseWfIn(args[0], bound, [], depth + 1);
557
+ return args.length !== 1 ? fault("arity") : wfChildren([1], []);
558
+ case "compound":
559
+ if (args.length < 1)
560
+ return fault("arity");
561
+ if (atomBytesOf(args[0]) === undefined)
562
+ return fault("argument", 1);
563
+ return wfChildren(all(2), []);
349
564
  case "map-of":
350
- if (args.length !== 2)
351
- return NOT_WF;
352
- return kleeneAll([
353
- diagnoseWfIn(args[0], bound, [], depth + 1),
354
- diagnoseWfIn(args[1], bound, [], depth + 1),
355
- ]);
565
+ return args.length !== 2 ? fault("arity") : wfChildren([1, 2], []);
356
566
  case "union":
357
567
  case "and":
358
- return wfAll(args, pending);
568
+ return wfChildren(all(1), pending);
359
569
  case "not":
360
- return args.length !== 1
361
- ? NOT_WF
362
- : diagnoseWfIn(args[0], bound, pending, depth + 1);
570
+ return args.length !== 1 ? fault("arity") : wfChildren([1], pending);
363
571
  case "rec": {
364
572
  if (args.length !== 2)
365
- return NOT_WF;
573
+ return fault("arity");
366
574
  const name = atomStringOf(args[0]);
367
575
  if (name === undefined)
368
- return NOT_WF;
369
- return diagnoseWfIn(args[1], [...bound, name], [...pending, name], depth + 1);
576
+ return fault("argument", 1);
577
+ return diagnoseWfIn(args[1], [...bound, name], [...pending, name], depth + 1, at(pos, 2));
370
578
  }
371
579
  case "ref": {
372
580
  if (args.length !== 1)
373
- return NOT_WF;
581
+ return fault("arity");
374
582
  const name = atomStringOf(args[0]);
375
- return name !== undefined && bound.includes(name) && !pending.includes(name)
376
- ? CONFORMS
377
- : NOT_WF;
583
+ if (name === undefined)
584
+ return fault("argument", 1);
585
+ if (!bound.includes(name))
586
+ return fault("unbound-ref", 1);
587
+ if (pending.includes(name))
588
+ return fault("unguarded-ref", 1);
589
+ return CONFORMS;
378
590
  }
379
591
  case "record": {
380
592
  if (args.length < 1)
381
- return NOT_WF;
593
+ return fault("arity");
382
594
  const mode = atomStringOf(args[0]);
383
595
  if (mode !== "open" && mode !== "closed")
384
- return NOT_WF;
596
+ return fault("argument", 1);
385
597
  const names = new Set();
386
- const outcomes = [];
387
- for (const field of args.slice(1)) {
388
- const fd = readFieldDescriptor(field);
389
- if (fd === undefined || names.has(fd.name))
390
- return NOT_WF;
598
+ const types = [];
599
+ for (let k = 2; k <= args.length; k += 1) {
600
+ const fd = readFieldDescriptor(args[k - 1]);
601
+ if (fd === undefined)
602
+ return fault("argument", k);
603
+ if (names.has(fd.name))
604
+ return fault("duplicate-field", k);
391
605
  names.add(fd.name);
392
- outcomes.push(diagnoseWfIn(fd.ty, bound, [], depth + 1));
606
+ types.push(fd.ty);
393
607
  }
394
- return kleeneAll(outcomes);
608
+ // Field k's type is item 3 of the field form at item k.
609
+ return kleeneAll(types.length, (i) => diagnoseWfIn(types[i], bound, [], depth + 1, at(at(pos, i + 2), 3)));
395
610
  }
396
611
  }
397
- return NOT_WF;
612
+ return fault("unknown-constructor");
398
613
  }
399
614
  //# sourceMappingURL=diagnose.js.map