@bitspark/horos 0.8.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.
- 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 +84 -55
- package/dist/src/core.js.map +1 -1
- package/dist/src/diagnose.d.ts +36 -3
- package/dist/src/diagnose.js +365 -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,75 @@ 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
|
-
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,
|
|
225
|
+
function diagnoseIn(v, t, env, depth, path, pos) {
|
|
165
226
|
if (depth > MAX_RECURSION_DEPTH)
|
|
166
|
-
return exhausted(
|
|
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
|
|
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 :
|
|
235
|
+
return leafClassifies(v, t) ? CONFORMS : refused();
|
|
174
236
|
switch (ctor) {
|
|
175
|
-
// Composition on the SAME v
|
|
237
|
+
// Composition on the SAME v.
|
|
176
238
|
case "union":
|
|
177
|
-
return kleeneAny(args.
|
|
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.
|
|
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
|
|
183
|
-
return kleeneNot(diagnoseIn(v, args[0], env,
|
|
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
|
|
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
|
|
253
|
+
return refused();
|
|
189
254
|
const name = atomStringOf(args[0]);
|
|
190
255
|
if (name === undefined)
|
|
191
|
-
return
|
|
256
|
+
return refused();
|
|
192
257
|
const body = args[1];
|
|
193
|
-
|
|
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
|
|
263
|
+
return refused();
|
|
198
264
|
const name = atomStringOf(args[0]);
|
|
199
265
|
if (name === undefined)
|
|
200
|
-
return
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
273
|
+
return refused(); // unbound
|
|
209
274
|
}
|
|
210
275
|
}
|
|
211
|
-
// Structural arms: the gate is decisive,
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
return
|
|
215
|
-
|
|
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;
|
|
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
|
|
296
|
+
return refused;
|
|
227
297
|
const items = v.items();
|
|
228
298
|
if (items.length !== args.length)
|
|
229
|
-
return
|
|
230
|
-
return items.map((c, 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
|
|
235
|
-
|
|
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
|
|
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
|
|
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
|
|
249
|
-
|
|
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
|
|
341
|
+
return refused;
|
|
342
|
+
let elems;
|
|
255
343
|
try {
|
|
256
|
-
|
|
257
|
-
return elems.map((e) => [e, args[0]]);
|
|
344
|
+
elems = ctor === "list-of" ? readList(v) : readSet(v);
|
|
258
345
|
}
|
|
259
346
|
catch {
|
|
260
|
-
return
|
|
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
|
|
359
|
+
return refused;
|
|
360
|
+
let entries;
|
|
266
361
|
try {
|
|
267
|
-
|
|
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
|
|
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
|
|
376
|
+
return args.length < 1
|
|
377
|
+
? refused
|
|
378
|
+
: recordChildren(v, args[0], args.slice(1), path, pos);
|
|
279
379
|
}
|
|
280
|
-
return
|
|
380
|
+
return refused;
|
|
281
381
|
}
|
|
282
|
-
/**
|
|
283
|
-
|
|
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
|
|
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
|
|
397
|
+
return refused;
|
|
294
398
|
}
|
|
295
399
|
const keyed = new Map();
|
|
296
|
-
for (
|
|
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
|
|
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 (
|
|
307
|
-
const fd = readFieldDescriptor(
|
|
415
|
+
for (let j = 0; j < fields.length; j += 1) {
|
|
416
|
+
const fd = readFieldDescriptor(fields[j]);
|
|
308
417
|
if (fd === undefined)
|
|
309
|
-
return
|
|
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
|
|
423
|
+
return { path, at: pos, reason: "missing-field", field: fd.name };
|
|
315
424
|
continue;
|
|
316
425
|
}
|
|
317
|
-
|
|
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
|
|
437
|
+
return { path, at: pos, reason: "extra-field", field: k };
|
|
323
438
|
}
|
|
324
439
|
}
|
|
325
440
|
return out;
|
|
326
441
|
}
|
|
327
|
-
|
|
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(
|
|
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
|
|
534
|
+
return fault("not-descriptor");
|
|
333
535
|
const { ctor, args } = h;
|
|
334
|
-
if (isLeafCtor(ctor))
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
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
|
|
573
|
+
return fault("arity");
|
|
366
574
|
const name = atomStringOf(args[0]);
|
|
367
575
|
if (name === undefined)
|
|
368
|
-
return
|
|
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
|
|
581
|
+
return fault("arity");
|
|
374
582
|
const name = atomStringOf(args[0]);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
593
|
+
return fault("arity");
|
|
382
594
|
const mode = atomStringOf(args[0]);
|
|
383
595
|
if (mode !== "open" && mode !== "closed")
|
|
384
|
-
return
|
|
596
|
+
return fault("argument", 1);
|
|
385
597
|
const names = new Set();
|
|
386
|
-
const
|
|
387
|
-
for (
|
|
388
|
-
const fd = readFieldDescriptor(
|
|
389
|
-
if (fd === undefined
|
|
390
|
-
return
|
|
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
|
-
|
|
606
|
+
types.push(fd.ty);
|
|
393
607
|
}
|
|
394
|
-
|
|
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
|
|
612
|
+
return fault("unknown-constructor");
|
|
398
613
|
}
|
|
399
614
|
//# sourceMappingURL=diagnose.js.map
|