@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.
- package/README.md +3 -3
- package/dist/src/cli.js +9 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/core.d.ts +1 -1
- package/dist/src/core.js +172 -62
- package/dist/src/core.js.map +1 -1
- package/dist/src/diagnose.d.ts +36 -3
- package/dist/src/diagnose.js +465 -150
- package/dist/src/diagnose.js.map +1 -1
- package/package.json +10 -10
package/dist/src/diagnose.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
return exhausted(limit);
|
|
92
|
-
return notWF ? NOT_WF : MISMATCH;
|
|
141
|
+
return firstExhausted ?? firstNotWf ?? CONFORMS;
|
|
93
142
|
}
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
127
|
-
if (w.verdict
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
|
308
|
+
function diagnoseIn(d, v, t, env, depth, path, pos, shared) {
|
|
165
309
|
if (depth > MAX_RECURSION_DEPTH)
|
|
166
|
-
return exhausted(
|
|
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
|
|
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 :
|
|
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
|
|
337
|
+
// Composition on the SAME v.
|
|
176
338
|
case "union":
|
|
177
|
-
return kleeneAny(args.
|
|
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.
|
|
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
|
|
183
|
-
return kleeneNot(diagnoseIn(v, args[0], env,
|
|
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
|
|
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
|
|
353
|
+
return refused();
|
|
189
354
|
const name = atomStringOf(args[0]);
|
|
190
355
|
if (name === undefined)
|
|
191
|
-
return
|
|
356
|
+
return refused();
|
|
192
357
|
const body = args[1];
|
|
193
|
-
|
|
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
|
|
363
|
+
return refused();
|
|
198
364
|
const name = atomStringOf(args[0]);
|
|
199
365
|
if (name === undefined)
|
|
200
|
-
return
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
373
|
+
return refused(); // unbound
|
|
209
374
|
}
|
|
210
375
|
}
|
|
211
|
-
// Structural arms: the gate is decisive,
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
return
|
|
215
|
-
|
|
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;
|
|
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
|
|
396
|
+
return refused;
|
|
227
397
|
const items = v.items();
|
|
228
398
|
if (items.length !== args.length)
|
|
229
|
-
return
|
|
230
|
-
return items.map((c, 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
|
|
235
|
-
|
|
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
|
|
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
|
|
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
|
|
249
|
-
|
|
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
|
|
441
|
+
return refused;
|
|
442
|
+
let elems;
|
|
255
443
|
try {
|
|
256
|
-
|
|
257
|
-
return elems.map((e) => [e, args[0]]);
|
|
444
|
+
elems = ctor === "list-of" ? readList(v) : readSet(v);
|
|
258
445
|
}
|
|
259
446
|
catch {
|
|
260
|
-
return
|
|
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
|
|
459
|
+
return refused;
|
|
460
|
+
let entries;
|
|
266
461
|
try {
|
|
267
|
-
|
|
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
|
|
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
|
|
476
|
+
return args.length < 1
|
|
477
|
+
? refused
|
|
478
|
+
: recordChildren(v, args[0], args.slice(1), path, pos);
|
|
279
479
|
}
|
|
280
|
-
return
|
|
480
|
+
return refused;
|
|
281
481
|
}
|
|
282
|
-
/**
|
|
283
|
-
|
|
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
|
|
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
|
|
497
|
+
return refused;
|
|
294
498
|
}
|
|
295
499
|
const keyed = new Map();
|
|
296
|
-
for (
|
|
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
|
|
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 (
|
|
307
|
-
const fd = readFieldDescriptor(
|
|
515
|
+
for (let j = 0; j < fields.length; j += 1) {
|
|
516
|
+
const fd = readFieldDescriptor(fields[j]);
|
|
308
517
|
if (fd === undefined)
|
|
309
|
-
return
|
|
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
|
|
523
|
+
return { path, at: pos, reason: "missing-field", field: fd.name };
|
|
315
524
|
continue;
|
|
316
525
|
}
|
|
317
|
-
|
|
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
|
|
537
|
+
return { path, at: pos, reason: "extra-field", field: k };
|
|
323
538
|
}
|
|
324
539
|
}
|
|
325
540
|
return out;
|
|
326
541
|
}
|
|
327
|
-
|
|
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(
|
|
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
|
|
634
|
+
return fault("not-descriptor");
|
|
333
635
|
const { ctor, args } = h;
|
|
334
|
-
if (isLeafCtor(ctor))
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
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
|
|
673
|
+
return fault("arity");
|
|
366
674
|
const name = atomStringOf(args[0]);
|
|
367
675
|
if (name === undefined)
|
|
368
|
-
return
|
|
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
|
|
681
|
+
return fault("arity");
|
|
374
682
|
const name = atomStringOf(args[0]);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
693
|
+
return fault("arity");
|
|
382
694
|
const mode = atomStringOf(args[0]);
|
|
383
695
|
if (mode !== "open" && mode !== "closed")
|
|
384
|
-
return
|
|
696
|
+
return fault("argument", 1);
|
|
385
697
|
const names = new Set();
|
|
386
|
-
const
|
|
387
|
-
for (
|
|
388
|
-
const fd = readFieldDescriptor(
|
|
389
|
-
if (fd === undefined
|
|
390
|
-
return
|
|
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
|
-
|
|
706
|
+
types.push(fd.ty);
|
|
393
707
|
}
|
|
394
|
-
|
|
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
|
|
712
|
+
return fault("unknown-constructor");
|
|
398
713
|
}
|
|
399
714
|
//# sourceMappingURL=diagnose.js.map
|