@bitspark/horos 0.9.0 → 0.11.1

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,158 @@ 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(new DiagChecker(), v, t, [], 0, undefined, undefined, false));
193
+ }
194
+ /**
195
+ * One diagnosis's memo (ADR 0013), kept only in shared mode: below a union or intersection
196
+ * with two or more alternatives over one value, where a sub-check can be reached along
197
+ * several routes. There each composite sub-check names its place by canonical path and
198
+ * position nodes: the first node seen per (parent, step) and per (parent, item index); a
199
+ * leaf keeps the plain node it was given. The node where shared mode begins is reached by
200
+ * one route, so its own nodes are unique too. Within a check a path node then names one value
201
+ * occurrence and a position node one node of the written descriptor, whose environment the
202
+ * position fixes (ADR 0011). A shared composite sub-check's outcome, location included, is
203
+ * a function of (path, position, depth), and `get`/`put` keep it. Outside shared mode the
204
+ * nodes are built and nothing is kept, as before ADR 0013.
205
+ */
206
+ class DiagChecker {
207
+ paths;
208
+ positions;
209
+ canonicalPaths;
210
+ canonicalPositions;
211
+ memo;
212
+ /**
213
+ * The canonical path node for `path`: the first seen for its canonical parent and step,
214
+ * `path` itself when it is the first. Nodes outside shared mode are reached by one
215
+ * route, so the first one seen for each step is canonical; the walk up stops at a node
216
+ * already seen.
217
+ */
218
+ canonPath(path) {
219
+ if (path === undefined || this.canonicalPaths?.has(path))
220
+ return path;
221
+ const parent = this.canonPath(path.parent);
222
+ this.paths ??= new Map();
223
+ this.canonicalPaths ??= new Set();
224
+ let children = this.paths.get(parent);
225
+ if (children === undefined) {
226
+ children = new Map();
227
+ this.paths.set(parent, children);
228
+ }
229
+ const s = path.step;
230
+ const key = s.kind === "field" ? `field:${s.name}` : `${s.kind}:${s.index}`;
231
+ const known = children.get(key);
232
+ if (known !== undefined)
233
+ return known;
234
+ const node = parent === path.parent ? path : stepInto(parent, s);
235
+ children.set(key, node);
236
+ this.canonicalPaths.add(node);
237
+ return node;
238
+ }
239
+ /** The canonical position node for `pos`, as {@link canonPath} for paths. */
240
+ canonPos(pos) {
241
+ if (pos === undefined || this.canonicalPositions?.has(pos))
242
+ return pos;
243
+ const parent = this.canonPos(pos.parent);
244
+ this.positions ??= new Map();
245
+ this.canonicalPositions ??= new Set();
246
+ let children = this.positions.get(parent);
247
+ if (children === undefined) {
248
+ children = new Map();
249
+ this.positions.set(parent, children);
250
+ }
251
+ const known = children.get(pos.index);
252
+ if (known !== undefined)
253
+ return known;
254
+ const node = parent === pos.parent ? pos : at(parent, pos.index);
255
+ children.set(pos.index, node);
256
+ this.canonicalPositions.add(node);
257
+ return node;
258
+ }
259
+ get(path, pos, depth) {
260
+ return this.memo?.get(path)?.get(pos)?.get(depth);
261
+ }
262
+ put(path, pos, depth, o) {
263
+ this.memo ??= new Map();
264
+ let byPos = this.memo.get(path);
265
+ if (byPos === undefined) {
266
+ byPos = new Map();
267
+ this.memo.set(path, byPos);
268
+ }
269
+ let byDepth = byPos.get(pos);
270
+ if (byDepth === undefined) {
271
+ byDepth = new Map();
272
+ byPos.set(pos, byDepth);
273
+ }
274
+ byDepth.set(depth, o);
275
+ }
132
276
  }
133
277
  function head(t) {
134
278
  if (t.kind !== "tuple")
@@ -161,239 +305,410 @@ function atomStringOf(v) {
161
305
  key += String.fromCharCode(byte);
162
306
  return key;
163
307
  }
164
- function diagnoseIn(v, t, env, pending, depth) {
308
+ function diagnoseIn(d, v, t, env, depth, path, pos, shared) {
165
309
  if (depth > MAX_RECURSION_DEPTH)
166
- return exhausted("recursion_depth");
310
+ return exhausted(path, pos);
311
+ const refused = () => mismatch({ path, at: pos, reason: "refused" });
167
312
  const h = head(t);
168
313
  if (h === undefined)
169
- return MISMATCH; // not a horos descriptor
314
+ return refused(); // not a horos descriptor; unreachable past the wf gate
170
315
  const { ctor, args } = h;
171
316
  // Leaves never recurse into a value position, so they are always decisive here.
172
317
  if (isLeafCtor(ctor))
173
- return leafClassifies(v, t) ? CONFORMS : MISMATCH;
318
+ return leafClassifies(v, t) ? CONFORMS : refused();
319
+ if (!shared)
320
+ return composite(d, v, ctor, args, env, depth, path, pos, false);
321
+ // Only a shared composite sub-check names its place canonically; a leaf never does.
322
+ const cpath = d.canonPath(path);
323
+ const cpos = d.canonPos(pos);
324
+ const hit = d.get(cpath, cpos, depth);
325
+ if (hit !== undefined)
326
+ return hit;
327
+ const o = composite(d, v, ctor, args, env, depth, cpath, cpos, true);
328
+ d.put(cpath, cpos, depth, o);
329
+ return o;
330
+ }
331
+ /** One composite descriptor node against `v`; `diagnoseIn` memoizes it. */
332
+ function composite(d, v, ctor, args, env, depth, path, pos, shared) {
333
+ // Two or more alternatives over one value: below them, shared mode (see DiagChecker).
334
+ const share = shared || args.length > 1;
335
+ const refused = () => mismatch({ path, at: pos, reason: "refused" });
174
336
  switch (ctor) {
175
- // Composition on the SAME v — pending threads through unchanged.
337
+ // Composition on the SAME v.
176
338
  case "union":
177
- return kleeneAny(args.map((ct) => diagnoseIn(v, ct, env, pending, depth + 1)));
339
+ return kleeneAny(args.length, (i) => diagnoseIn(d, v, args[i], env, depth + 1, path, at(pos, i + 1), share), path, pos);
178
340
  case "and":
179
- return kleeneAll(args.map((ct) => diagnoseIn(v, ct, env, pending, depth + 1)));
341
+ return kleeneAll(args.length, (i) => diagnoseIn(d, v, args[i], env, depth + 1, path, at(pos, i + 1), share));
180
342
  case "not": {
181
343
  if (args.length !== 1)
182
- return MISMATCH;
183
- return kleeneNot(diagnoseIn(v, args[0], env, pending, depth + 1));
344
+ return refused();
345
+ return kleeneNot(diagnoseIn(d, v, args[0], env, depth + 1, path, at(pos, 1), shared), path, pos);
184
346
  }
185
- // Recursion — mirrors the recognizer's guard exactly.
347
+ // Recursion — mirrors the recognizer, and is transparent to locations. A ref denotes
348
+ // the rec that lexically encloses it and continues in that binding's environment, one
349
+ // counted frame deeper (ADR 0011). `env` holds exactly the enclosing bindings,
350
+ // outermost first, so a binding's closure is the prefix of `env` that ends with it.
186
351
  case "rec": {
187
352
  if (args.length !== 2)
188
- return MISMATCH;
353
+ return refused();
189
354
  const name = atomStringOf(args[0]);
190
355
  if (name === undefined)
191
- return MISMATCH;
356
+ return refused();
192
357
  const body = args[1];
193
- return diagnoseIn(v, body, [...env, { name, body }], [...pending, name], depth + 1);
358
+ const bodyAt = at(pos, 2);
359
+ return diagnoseIn(d, v, body, [...env, { name, body, at: bodyAt }], depth + 1, path, bodyAt, shared);
194
360
  }
195
361
  case "ref": {
196
362
  if (args.length !== 1)
197
- return MISMATCH;
363
+ return refused();
198
364
  const name = atomStringOf(args[0]);
199
365
  if (name === undefined)
200
- return MISMATCH;
201
- if (pending.includes(name))
202
- return MISMATCH; // unguarded — the recognizer's false
366
+ return refused();
203
367
  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);
368
+ const b = env[i];
369
+ if (b.name === name) {
370
+ return diagnoseIn(d, v, b.body, env.slice(0, i + 1), depth + 1, path, b.at, shared);
206
371
  }
207
372
  }
208
- return MISMATCH; // unbound
373
+ return refused(); // unbound
209
374
  }
210
375
  }
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)));
376
+ // Structural arms: the gate is decisive, then each child value against its descriptor.
377
+ const gate = structuralChildren(v, ctor, args, path, pos);
378
+ if (!Array.isArray(gate))
379
+ return mismatch(gate);
380
+ const children = gate;
381
+ return kleeneAll(children.length, (i) => {
382
+ const c = children[i];
383
+ return diagnoseIn(d, c.v, c.t, env, depth + 1, stepInto(path, c.step), c.at, shared);
384
+ });
216
385
  }
217
386
  /**
218
387
  * 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.
388
+ * child VALUE. Mirrors the recognizer's recursive arms; a {@link Fault} means the gate failed,
389
+ * so `v` cannot conform regardless of any child.
221
390
  */
222
- function structuralChildren(v, ctor, args) {
391
+ function structuralChildren(v, ctor, args, path, pos) {
392
+ const refused = { path, at: pos, reason: "refused" };
223
393
  switch (ctor) {
224
394
  case "tuple": {
225
395
  if (v.kind !== "tuple")
226
- return undefined;
396
+ return refused;
227
397
  const items = v.items();
228
398
  if (items.length !== args.length)
229
- return undefined;
230
- return items.map((c, i) => [c, args[i]]);
399
+ return refused;
400
+ return items.map((c, i) => ({
401
+ v: c,
402
+ t: args[i],
403
+ step: { kind: "item", index: i },
404
+ at: at(pos, i + 1),
405
+ }));
231
406
  }
232
407
  case "seq": {
233
408
  if (args.length !== 1 || v.kind !== "tuple")
234
- return undefined;
235
- return v.items().map((c) => [c, args[0]]);
409
+ return refused;
410
+ const childAt = at(pos, 1);
411
+ return v.items().map((c, i) => ({
412
+ v: c,
413
+ t: args[0],
414
+ step: { kind: "item", index: i },
415
+ at: childAt,
416
+ }));
236
417
  }
237
418
  case "compound": {
238
419
  if (args.length < 1 || v.kind !== "tuple")
239
- return undefined;
420
+ return refused;
240
421
  const label = atomBytesOf(args[0]);
241
422
  const items = v.items();
242
423
  if (label === undefined || items.length !== args.length)
243
- return undefined;
424
+ return refused;
244
425
  const got = atomBytesOf(items[0]);
245
426
  if (got === undefined ||
246
427
  got.length !== label.length ||
247
428
  got.some((b, i) => b !== label[i]))
248
- return undefined;
249
- return args.slice(1).map((ct, i) => [items[i + 1], ct]);
429
+ return refused;
430
+ // Value item j (j >= 1) is checked by descriptor argument j, at item index j + 1.
431
+ return args.slice(1).map((ct, i) => ({
432
+ v: items[i + 1],
433
+ t: ct,
434
+ step: { kind: "item", index: i + 1 },
435
+ at: at(pos, i + 2),
436
+ }));
250
437
  }
251
438
  case "list-of":
252
439
  case "set-of": {
253
440
  if (args.length !== 1)
254
- return undefined;
441
+ return refused;
442
+ let elems;
255
443
  try {
256
- const elems = ctor === "list-of" ? readList(v) : readSet(v);
257
- return elems.map((e) => [e, args[0]]);
444
+ elems = ctor === "list-of" ? readList(v) : readSet(v);
258
445
  }
259
446
  catch {
260
- return undefined;
447
+ return refused;
261
448
  }
449
+ const childAt = at(pos, 1);
450
+ return elems.map((e, i) => ({
451
+ v: e,
452
+ t: args[0],
453
+ step: { kind: "elem", index: i },
454
+ at: childAt,
455
+ }));
262
456
  }
263
457
  case "map-of": {
264
458
  if (args.length !== 2)
265
- return undefined;
459
+ return refused;
460
+ let entries;
266
461
  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;
462
+ entries = readMap(v);
272
463
  }
273
464
  catch {
274
- return undefined;
465
+ return refused;
275
466
  }
467
+ const keyAt = at(pos, 1);
468
+ const valueAt = at(pos, 2);
469
+ const out = [];
470
+ entries.forEach(([k, val], i) => {
471
+ 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 });
472
+ });
473
+ return out;
276
474
  }
277
475
  case "record":
278
- return args.length < 1 ? undefined : recordChildren(v, args[0], args.slice(1));
476
+ return args.length < 1
477
+ ? refused
478
+ : recordChildren(v, args[0], args.slice(1), path, pos);
279
479
  }
280
- return undefined;
480
+ return refused;
281
481
  }
282
- /** Mirrors the recognizer's record gate and present-field worklist. */
283
- function recordChildren(v, mode, fields) {
482
+ /**
483
+ * Mirrors the recognizer's record gate and present-field worklist, in the fixed order of
484
+ * ADR 0009: a canonical map, text keys, missing required fields in descriptor order, extra
485
+ * fields of a closed record in canonical key order, then the present declared fields.
486
+ */
487
+ function recordChildren(v, mode, fields, path, pos) {
488
+ const refused = { path, at: pos, reason: "refused" };
284
489
  const m = atomStringOf(mode);
285
490
  if (m !== "open" && m !== "closed")
286
- return undefined;
287
- const closed = m === "closed";
491
+ return refused;
288
492
  let entries;
289
493
  try {
290
494
  entries = readMap(v);
291
495
  }
292
496
  catch {
293
- return undefined;
497
+ return refused;
294
498
  }
295
499
  const keyed = new Map();
296
- for (const [k, val] of entries) {
500
+ for (let i = 0; i < entries.length; i += 1) {
501
+ const [k, val] = entries[i];
297
502
  try {
298
503
  keyed.set(readText(k), val);
299
504
  }
300
505
  catch {
301
- return undefined;
506
+ return {
507
+ path: stepInto(path, { kind: "key", index: i }),
508
+ at: pos,
509
+ reason: "refused",
510
+ };
302
511
  }
303
512
  }
304
513
  const out = [];
305
514
  const declared = new Set();
306
- for (const f of fields) {
307
- const fd = readFieldDescriptor(f);
515
+ for (let j = 0; j < fields.length; j += 1) {
516
+ const fd = readFieldDescriptor(fields[j]);
308
517
  if (fd === undefined)
309
- return undefined;
518
+ return refused;
310
519
  declared.add(fd.name);
311
520
  const val = keyed.get(fd.name);
312
521
  if (val === undefined) {
313
522
  if (fd.required)
314
- return undefined; // a missing required field is decisive
523
+ return { path, at: pos, reason: "missing-field", field: fd.name };
315
524
  continue;
316
525
  }
317
- out.push([val, fd.ty]);
526
+ // Field j is descriptor item j + 2; its type is item 3 of the field form.
527
+ out.push({
528
+ v: val,
529
+ t: fd.ty,
530
+ step: { kind: "field", name: fd.name },
531
+ at: at(at(pos, j + 2), 3),
532
+ });
318
533
  }
319
- if (closed) {
534
+ if (m === "closed") {
320
535
  for (const k of keyed.keys()) {
321
536
  if (!declared.has(k))
322
- return undefined; // an extra key in a closed record is decisive
537
+ return { path, at: pos, reason: "extra-field", field: k };
323
538
  }
324
539
  }
325
540
  return out;
326
541
  }
327
- function diagnoseWfIn(t, bound, pending, depth) {
542
+ /** In-range check of an `atom-len`/`text-len` bound: the signed 128-bit cap. */
543
+ function boundInRange(n) {
544
+ return n >= ATOM_LEN_BOUND_MIN && n <= ATOM_LEN_BOUND_MAX;
545
+ }
546
+ /**
547
+ * The leaf constructors' own-form check, in the fixed order of ADR 0009: arity, then each
548
+ * argument in item order against the arguments before it. `undefined` is well formed;
549
+ * otherwise the reason and, for an argument, its item index. Mirrors the recognizer's
550
+ * `wfIn` leaf arms, which decide the same set.
551
+ */
552
+ function leafWfFault(ctor, args) {
553
+ const arity = (n) => args.length === n ? undefined : { reason: "arity" };
554
+ const argument = (i) => ({ reason: "argument", arg: i });
555
+ switch (ctor) {
556
+ case "any":
557
+ case "atom":
558
+ case "key-grammar":
559
+ case "text-non-blank":
560
+ case "lowercase-uuid":
561
+ return arity(0);
562
+ case "exact":
563
+ return arity(1);
564
+ case "atom-exact":
565
+ return arity(1) ?? (atomBytesOf(args[0]) === undefined ? argument(1) : undefined);
566
+ case "int-min":
567
+ case "int-max": {
568
+ const a = arity(1);
569
+ if (a !== undefined)
570
+ return a;
571
+ try {
572
+ return boundInRange(readInt(args[0])) ? undefined : argument(1);
573
+ }
574
+ catch {
575
+ return argument(1);
576
+ }
577
+ }
578
+ case "atom-len":
579
+ case "text-len": {
580
+ const a = arity(2);
581
+ if (a !== undefined)
582
+ return a;
583
+ let lo;
584
+ try {
585
+ lo = readInt(args[0]);
586
+ }
587
+ catch {
588
+ return argument(1);
589
+ }
590
+ if (!boundInRange(lo) || lo < 0n)
591
+ return argument(1);
592
+ let hi;
593
+ try {
594
+ hi = readInt(args[1]);
595
+ }
596
+ catch {
597
+ return argument(2);
598
+ }
599
+ return !boundInRange(hi) || hi < lo ? argument(2) : undefined;
600
+ }
601
+ case "atom-in": {
602
+ const a = arity(1);
603
+ if (a !== undefined)
604
+ return a;
605
+ let elems;
606
+ try {
607
+ elems = readSet(args[0]);
608
+ }
609
+ catch {
610
+ return argument(1);
611
+ }
612
+ return elems.every((e) => e.kind === "atom") ? undefined : argument(1);
613
+ }
614
+ case "scalar": {
615
+ const a = arity(1);
616
+ if (a !== undefined)
617
+ return a;
618
+ const kind = atomStringOf(args[0]);
619
+ return kind === "int" || kind === "utf8-text" || kind === "bool"
620
+ ? undefined
621
+ : argument(1);
622
+ }
623
+ }
624
+ return { reason: "unknown-constructor" };
625
+ }
626
+ function diagnoseWfIn(t, bound, pending, depth, pos) {
328
627
  if (depth > MAX_RECURSION_DEPTH)
329
- return exhausted("recursion_depth");
628
+ return exhausted(undefined, pos);
629
+ const fault = (reason, arg) => notWf(arg === undefined
630
+ ? { path: undefined, at: pos, reason }
631
+ : { path: undefined, at: pos, reason, arg });
330
632
  const h = head(t);
331
633
  if (h === undefined)
332
- return NOT_WF;
634
+ return fault("not-descriptor");
333
635
  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)));
636
+ if (isLeafCtor(ctor)) {
637
+ // The recognizer decides the verdict; leafWfFault only names the fault.
638
+ if (leafWellFormed(t))
639
+ return CONFORMS;
640
+ const f = leafWfFault(ctor, args) ?? { reason: "argument" };
641
+ return fault(f.reason, f.arg);
642
+ }
643
+ // Child descriptors at the given item indices, in order, after the own form is accepted.
644
+ const wfChildren = (items, p) => kleeneAll(items.length, (i) => diagnoseWfIn(args[items[i] - 1], bound, p, depth + 1, at(pos, items[i])));
645
+ const all = (from) => {
646
+ const out = [];
647
+ for (let i = from; i <= args.length; i += 1)
648
+ out.push(i);
649
+ return out;
650
+ };
337
651
  switch (ctor) {
338
652
  case "tuple":
339
- return wfAll(args, []);
653
+ return wfChildren(all(1), []);
340
654
  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
655
  case "list-of":
347
656
  case "set-of":
348
- return args.length !== 1 ? NOT_WF : diagnoseWfIn(args[0], bound, [], depth + 1);
657
+ return args.length !== 1 ? fault("arity") : wfChildren([1], []);
658
+ case "compound":
659
+ if (args.length < 1)
660
+ return fault("arity");
661
+ if (atomBytesOf(args[0]) === undefined)
662
+ return fault("argument", 1);
663
+ return wfChildren(all(2), []);
349
664
  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
- ]);
665
+ return args.length !== 2 ? fault("arity") : wfChildren([1, 2], []);
356
666
  case "union":
357
667
  case "and":
358
- return wfAll(args, pending);
668
+ return wfChildren(all(1), pending);
359
669
  case "not":
360
- return args.length !== 1
361
- ? NOT_WF
362
- : diagnoseWfIn(args[0], bound, pending, depth + 1);
670
+ return args.length !== 1 ? fault("arity") : wfChildren([1], pending);
363
671
  case "rec": {
364
672
  if (args.length !== 2)
365
- return NOT_WF;
673
+ return fault("arity");
366
674
  const name = atomStringOf(args[0]);
367
675
  if (name === undefined)
368
- return NOT_WF;
369
- return diagnoseWfIn(args[1], [...bound, name], [...pending, name], depth + 1);
676
+ return fault("argument", 1);
677
+ return diagnoseWfIn(args[1], [...bound, name], [...pending, name], depth + 1, at(pos, 2));
370
678
  }
371
679
  case "ref": {
372
680
  if (args.length !== 1)
373
- return NOT_WF;
681
+ return fault("arity");
374
682
  const name = atomStringOf(args[0]);
375
- return name !== undefined && bound.includes(name) && !pending.includes(name)
376
- ? CONFORMS
377
- : NOT_WF;
683
+ if (name === undefined)
684
+ return fault("argument", 1);
685
+ if (!bound.includes(name))
686
+ return fault("unbound-ref", 1);
687
+ if (pending.includes(name))
688
+ return fault("unguarded-ref", 1);
689
+ return CONFORMS;
378
690
  }
379
691
  case "record": {
380
692
  if (args.length < 1)
381
- return NOT_WF;
693
+ return fault("arity");
382
694
  const mode = atomStringOf(args[0]);
383
695
  if (mode !== "open" && mode !== "closed")
384
- return NOT_WF;
696
+ return fault("argument", 1);
385
697
  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;
698
+ const types = [];
699
+ for (let k = 2; k <= args.length; k += 1) {
700
+ const fd = readFieldDescriptor(args[k - 1]);
701
+ if (fd === undefined)
702
+ return fault("argument", k);
703
+ if (names.has(fd.name))
704
+ return fault("duplicate-field", k);
391
705
  names.add(fd.name);
392
- outcomes.push(diagnoseWfIn(fd.ty, bound, [], depth + 1));
706
+ types.push(fd.ty);
393
707
  }
394
- return kleeneAll(outcomes);
708
+ // Field k's type is item 3 of the field form at item k.
709
+ return kleeneAll(types.length, (i) => diagnoseWfIn(types[i], bound, [], depth + 1, at(at(pos, i + 2), 3)));
395
710
  }
396
711
  }
397
- return NOT_WF;
712
+ return fault("unknown-constructor");
398
713
  }
399
714
  //# sourceMappingURL=diagnose.js.map