@json-schema-engine/compiler 0.0.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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/emit.d.ts +52 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +166 -0
- package/dist/emit.js.map +1 -0
- package/dist/explain.d.ts +28 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +42 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/plan.d.ts +83 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +385 -0
- package/dist/plan.js.map +1 -0
- package/dist/runtime-compile.d.ts +30 -0
- package/dist/runtime-compile.d.ts.map +1 -0
- package/dist/runtime-compile.js +29 -0
- package/dist/runtime-compile.js.map +1 -0
- package/dist/runtime.d.ts +157 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +336 -0
- package/dist/runtime.js.map +1 -0
- package/dist/serialize/apply.d.ts +28 -0
- package/dist/serialize/apply.d.ts.map +1 -0
- package/dist/serialize/apply.js +211 -0
- package/dist/serialize/apply.js.map +1 -0
- package/dist/serialize/context.d.ts +143 -0
- package/dist/serialize/context.d.ts.map +1 -0
- package/dist/serialize/context.js +93 -0
- package/dist/serialize/context.js.map +1 -0
- package/dist/serialize/expressions.d.ts +7 -0
- package/dist/serialize/expressions.d.ts.map +1 -0
- package/dist/serialize/expressions.js +177 -0
- package/dist/serialize/expressions.js.map +1 -0
- package/dist/serialize/guards.d.ts +7 -0
- package/dist/serialize/guards.d.ts.map +1 -0
- package/dist/serialize/guards.js +19 -0
- package/dist/serialize/guards.js.map +1 -0
- package/dist/serialize/index.d.ts +7 -0
- package/dist/serialize/index.d.ts.map +1 -0
- package/dist/serialize/index.js +266 -0
- package/dist/serialize/index.js.map +1 -0
- package/dist/serialize/keywords.d.ts +32 -0
- package/dist/serialize/keywords.d.ts.map +1 -0
- package/dist/serialize/keywords.js +131 -0
- package/dist/serialize/keywords.js.map +1 -0
- package/dist/serialize/messages.d.ts +36 -0
- package/dist/serialize/messages.d.ts.map +1 -0
- package/dist/serialize/messages.js +100 -0
- package/dist/serialize/messages.js.map +1 -0
- package/dist/serialize/names.d.ts +47 -0
- package/dist/serialize/names.d.ts.map +1 -0
- package/dist/serialize/names.js +115 -0
- package/dist/serialize/names.js.map +1 -0
- package/dist/serialize/spans.d.ts +34 -0
- package/dist/serialize/spans.d.ts.map +1 -0
- package/dist/serialize/spans.js +61 -0
- package/dist/serialize/spans.js.map +1 -0
- package/dist/serialize/statements.d.ts +16 -0
- package/dist/serialize/statements.d.ts.map +1 -0
- package/dist/serialize/statements.js +396 -0
- package/dist/serialize/statements.js.map +1 -0
- package/dist/serialize/unit.d.ts +8 -0
- package/dist/serialize/unit.d.ts.map +1 -0
- package/dist/serialize/unit.js +107 -0
- package/dist/serialize/unit.js.map +1 -0
- package/dist/standalone.d.ts +24 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +166 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +38 -0
- package/src/emit.ts +196 -0
- package/src/explain.ts +67 -0
- package/src/index.ts +366 -0
- package/src/plan.ts +496 -0
- package/src/runtime-compile.ts +90 -0
- package/src/runtime.ts +599 -0
- package/src/serialize/apply.ts +251 -0
- package/src/serialize/context.ts +161 -0
- package/src/serialize/expressions.ts +202 -0
- package/src/serialize/guards.ts +20 -0
- package/src/serialize/index.ts +432 -0
- package/src/serialize/keywords.ts +144 -0
- package/src/serialize/messages.ts +120 -0
- package/src/serialize/names.ts +136 -0
- package/src/serialize/spans.ts +88 -0
- package/src/serialize/statements.ts +531 -0
- package/src/serialize/unit.ts +118 -0
- package/src/standalone.ts +187 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// LowerStmt rendering: the per-keyword statement list with its grouped
|
|
2
|
+
// anyOf/oneOf runs, every statement kind, and the apply folds.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type JsonValue,
|
|
6
|
+
type LowerApply,
|
|
7
|
+
type LowerMessage,
|
|
8
|
+
type LowerParams,
|
|
9
|
+
type LowerStmt,
|
|
10
|
+
} from "@json-schema-engine/core";
|
|
11
|
+
import { type CodeChunk, id, join, js, num, json } from "../emit.js";
|
|
12
|
+
import { EV, bindingVar, counterVar, foldVar } from "./names.js";
|
|
13
|
+
import { UnitContext, SerializeError } from "./context.js";
|
|
14
|
+
import {
|
|
15
|
+
renderMessage,
|
|
16
|
+
paramsChunk,
|
|
17
|
+
pushError,
|
|
18
|
+
errMark,
|
|
19
|
+
errsLength,
|
|
20
|
+
errsCut,
|
|
21
|
+
} from "./messages.js";
|
|
22
|
+
import {
|
|
23
|
+
channelSpans,
|
|
24
|
+
spanDecls,
|
|
25
|
+
spanResets,
|
|
26
|
+
branchSpan,
|
|
27
|
+
annsLength,
|
|
28
|
+
annsCut,
|
|
29
|
+
annsPush,
|
|
30
|
+
} from "./spans.js";
|
|
31
|
+
import { annUnit, annRecordSegment, produceChannel } from "./keywords.js";
|
|
32
|
+
import { expr } from "./expressions.js";
|
|
33
|
+
import { applyCall, tryInline } from "./apply.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Serialize one keyword's statement list. Runs of anyMayPass applies
|
|
37
|
+
* (the anyOf shape) group into a single OR check; short-circuit emission
|
|
38
|
+
* is licensed here because the planner interpreted every node whose
|
|
39
|
+
* channel a consumer could observe (slice licensing, DESIGN §7). Runs of
|
|
40
|
+
* exactlyOne applies (the oneOf shape) group into a counter block that
|
|
41
|
+
* runs EVERY branch — never stop-at-first-success, since the count past 1
|
|
42
|
+
* must still be exact.
|
|
43
|
+
*/
|
|
44
|
+
export function keywordStatements(
|
|
45
|
+
ctx: UnitContext,
|
|
46
|
+
stmts: readonly LowerStmt[],
|
|
47
|
+
): CodeChunk[] {
|
|
48
|
+
const out: CodeChunk[] = [];
|
|
49
|
+
let anyRun: CodeChunk[] = [];
|
|
50
|
+
let oneRun: CodeChunk[] = [];
|
|
51
|
+
const flushAny = (message?: LowerMessage, params?: LowerParams) => {
|
|
52
|
+
if (anyRun.length === 0) return;
|
|
53
|
+
if (ctx.output === "list") {
|
|
54
|
+
// Every branch runs (§7: list artifacts never short-circuit), each
|
|
55
|
+
// marking/truncating its channel spans (branchSpan). An accepting
|
|
56
|
+
// run drops every branch's errors (errMark).
|
|
57
|
+
const a = counterVar(ctx.counters.tally++);
|
|
58
|
+
const em = errMark(ctx)!;
|
|
59
|
+
const runs = anyRun.map((call) =>
|
|
60
|
+
branchSpan(ctx, call, js`${a} = true;`),
|
|
61
|
+
);
|
|
62
|
+
const onFail = pushError(
|
|
63
|
+
ctx,
|
|
64
|
+
renderMessage(ctx, message ?? ["no branch matched"]),
|
|
65
|
+
true,
|
|
66
|
+
paramsChunk(ctx, params),
|
|
67
|
+
);
|
|
68
|
+
out.push(
|
|
69
|
+
js`let ${a} = false; const ${em} = ${errsLength(ctx)}; ${join(" ", runs)} if (${a}) { ${errsCut(ctx, em)} } else { ${onFail} }`,
|
|
70
|
+
);
|
|
71
|
+
} else if (ctx.regionMode) {
|
|
72
|
+
// Every branch runs (no short-circuit — a later branch's success
|
|
73
|
+
// contributes coverage the interpreter would merge), each marking and
|
|
74
|
+
// truncating its own channel span (rule 2). Flag failure on no match.
|
|
75
|
+
const a = counterVar(ctx.counters.tally++);
|
|
76
|
+
const runs = anyRun.map((call) =>
|
|
77
|
+
branchSpan(ctx, call, js`${a} = true;`),
|
|
78
|
+
);
|
|
79
|
+
out.push(
|
|
80
|
+
js`let ${a} = false; ${join(" ", runs)} if (!${a}) return false;`,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
out.push(js`if (!(${join(" || ", anyRun)})) return false;`);
|
|
84
|
+
}
|
|
85
|
+
anyRun = [];
|
|
86
|
+
};
|
|
87
|
+
const flushOne = (message?: LowerMessage, params?: LowerParams) => {
|
|
88
|
+
if (oneRun.length === 0) return;
|
|
89
|
+
const c = counterVar(ctx.counters.tally++);
|
|
90
|
+
if (ctx.output === "list") {
|
|
91
|
+
// Params referencing the passing-branch indexes (tallyList) need an
|
|
92
|
+
// index accumulator next to the count; branch order IS run order.
|
|
93
|
+
// Both-pass discards at the caller (the unit fails, its span
|
|
94
|
+
// truncates); each branch marks/truncates its own spans here.
|
|
95
|
+
const wantsList =
|
|
96
|
+
ctx.listParams &&
|
|
97
|
+
params !== undefined &&
|
|
98
|
+
Object.values(params).some((p) => p.kind === "tallyList");
|
|
99
|
+
const p = wantsList ? counterVar(ctx.counters.tally++) : undefined;
|
|
100
|
+
const incs = oneRun.map((call, k) =>
|
|
101
|
+
branchSpan(
|
|
102
|
+
ctx,
|
|
103
|
+
call,
|
|
104
|
+
p ? js`${c}++; ${p}.push(${num(k)});` : js`${c}++;`,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
const decl = p ? js`let ${c} = 0; const ${p} = [];` : js`let ${c} = 0;`;
|
|
108
|
+
const onFail = pushError(
|
|
109
|
+
ctx,
|
|
110
|
+
renderMessage(
|
|
111
|
+
ctx,
|
|
112
|
+
message ?? [{ kind: "tally" }, " branches matched"],
|
|
113
|
+
c,
|
|
114
|
+
),
|
|
115
|
+
true,
|
|
116
|
+
paramsChunk(ctx, params, c, p),
|
|
117
|
+
);
|
|
118
|
+
const em = errMark(ctx)!;
|
|
119
|
+
out.push(
|
|
120
|
+
js`${decl} const ${em} = ${errsLength(ctx)}; ${join(" ", incs)} if (${c} === 1) { ${errsCut(ctx, em)} } else { ${onFail} }`,
|
|
121
|
+
);
|
|
122
|
+
} else if (ctx.regionMode) {
|
|
123
|
+
// Run every branch with a per-branch mark/truncate (rule 2): a passing
|
|
124
|
+
// branch's coverage merges, a failing branch's truncates. On a non-unit
|
|
125
|
+
// count the unit fails and the caller truncates the whole span.
|
|
126
|
+
const incs = oneRun.map((call) => branchSpan(ctx, call, js`${c}++;`));
|
|
127
|
+
out.push(
|
|
128
|
+
js`let ${c} = 0; ${join(" ", incs)} if (${c} !== 1) return false;`,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
const incs = oneRun.map((call) => js`if (${call}) ${c}++;`);
|
|
132
|
+
out.push(
|
|
133
|
+
js`let ${c} = 0; ${join(" ", incs)} if (${c} !== 1) return false;`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
oneRun = [];
|
|
137
|
+
};
|
|
138
|
+
for (const stmt of stmts) {
|
|
139
|
+
if (stmt.kind === "apply" && stmt.apply.fold === "anyMayPass") {
|
|
140
|
+
flushOne();
|
|
141
|
+
anyRun.push(applyCall(ctx, stmt.apply));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (stmt.kind === "apply" && stmt.apply.fold === "exactlyOne") {
|
|
145
|
+
flushAny();
|
|
146
|
+
oneRun.push(applyCall(ctx, stmt.apply));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (stmt.kind === "combineCheck") {
|
|
150
|
+
// Closes the pending run with the keyword's own failure message.
|
|
151
|
+
flushAny(stmt.message, stmt.params);
|
|
152
|
+
flushOne(stmt.message, stmt.params);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
flushAny();
|
|
156
|
+
flushOne();
|
|
157
|
+
out.push(statement(ctx, stmt));
|
|
158
|
+
}
|
|
159
|
+
flushAny();
|
|
160
|
+
flushOne();
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** An `if` statement; an applyExpr condition is hoisted so its channel spans and error mark bracket the call. */
|
|
165
|
+
function emitIf(
|
|
166
|
+
ctx: UnitContext,
|
|
167
|
+
stmt: Extract<LowerStmt, { kind: "if" }>,
|
|
168
|
+
): CodeChunk {
|
|
169
|
+
// `if`'s condition IS a bare applyExpr (the only such shape besides
|
|
170
|
+
// contains' probe): hoist the call so its span marks/truncates before
|
|
171
|
+
// the branch reads the verdict. Any other applyExpr position throws in
|
|
172
|
+
// annotation mode (expr()), so no silent annotation loss.
|
|
173
|
+
if (
|
|
174
|
+
(ctx.annMode || ctx.regionMode || ctx.output === "list") &&
|
|
175
|
+
stmt.cond.kind === "applyExpr"
|
|
176
|
+
) {
|
|
177
|
+
// `if`'s condition is in-place: hoist the call so its channel spans
|
|
178
|
+
// mark/truncate on the condition verdict (a passing condition's
|
|
179
|
+
// coverage merges, a failing one's — with the else taken — truncates;
|
|
180
|
+
// rule 4). ann marks `anns`, region marks `ev`, composed both. The
|
|
181
|
+
// condition's errors are always irrelevant (`if` accepts regardless
|
|
182
|
+
// of its subschema), so list mode truncates them unconditionally.
|
|
183
|
+
const spans = channelSpans(ctx);
|
|
184
|
+
const t = id("m" + String(ctx.counters.temp++));
|
|
185
|
+
const em = errMark(ctx);
|
|
186
|
+
const call = applyCall(ctx, stmt.cond.apply);
|
|
187
|
+
const errDecl = em ? js`const ${em} = ${errsLength(ctx)}; ` : js``;
|
|
188
|
+
const errReset = em ? js` ${errsCut(ctx, em)}` : js``;
|
|
189
|
+
const resets =
|
|
190
|
+
spans.length > 0 ? js` if (!${t}) { ${spanResets(ctx, spans)} }` : js``;
|
|
191
|
+
const head = js`${spanDecls(ctx, spans)} ${errDecl}const ${t} = ${call};${errReset}${resets}`;
|
|
192
|
+
const thenBody = join(
|
|
193
|
+
"\n",
|
|
194
|
+
stmt.then.map((s) => statement(ctx, s)),
|
|
195
|
+
);
|
|
196
|
+
if (!stmt.else) return js`${head} if (${t}) { ${thenBody} }`;
|
|
197
|
+
const elseBody = join(
|
|
198
|
+
"\n",
|
|
199
|
+
stmt.else.map((s) => statement(ctx, s)),
|
|
200
|
+
);
|
|
201
|
+
return js`${head} if (${t}) { ${thenBody} } else { ${elseBody} }`;
|
|
202
|
+
}
|
|
203
|
+
const thenBody = join(
|
|
204
|
+
"\n",
|
|
205
|
+
stmt.then.map((s) => statement(ctx, s)),
|
|
206
|
+
);
|
|
207
|
+
const cond = js`if (${expr(ctx, stmt.cond)}) { ${thenBody} }`;
|
|
208
|
+
if (!stmt.else) return cond;
|
|
209
|
+
const elseBody = join(
|
|
210
|
+
"\n",
|
|
211
|
+
stmt.else.map((s) => statement(ctx, s)),
|
|
212
|
+
);
|
|
213
|
+
return js`${cond} else { ${elseBody} }`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A sweep over an object's own keys (plain for-in under the plain-data contract). */
|
|
217
|
+
function emitForEachKey(
|
|
218
|
+
ctx: UnitContext,
|
|
219
|
+
stmt: Extract<LowerStmt, { kind: "forEachKey" }>,
|
|
220
|
+
): CodeChunk {
|
|
221
|
+
const b = bindingVar(stmt.binding);
|
|
222
|
+
const body = join(
|
|
223
|
+
"\n",
|
|
224
|
+
stmt.body.map((s) => statement(ctx, s)),
|
|
225
|
+
);
|
|
226
|
+
// Plain for-in: under the plain-data instance contract (DESIGN §7)
|
|
227
|
+
// instances carry no inherited enumerables, so for-in ≡ Object.keys
|
|
228
|
+
// without the per-validation array allocation.
|
|
229
|
+
const t = expr(ctx, stmt.target);
|
|
230
|
+
if (ctx.flags.plainData) {
|
|
231
|
+
return js`for (const ${b} in ${t}) { ${body} }`;
|
|
232
|
+
}
|
|
233
|
+
return js`for (const ${b} of Object.keys(${t})) { ${body} }`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A sweep over an array's indexes. */
|
|
237
|
+
function emitForEachIndex(
|
|
238
|
+
ctx: UnitContext,
|
|
239
|
+
stmt: Extract<LowerStmt, { kind: "forEachIndex" }>,
|
|
240
|
+
): CodeChunk {
|
|
241
|
+
const b = bindingVar(stmt.binding);
|
|
242
|
+
const body = join(
|
|
243
|
+
"\n",
|
|
244
|
+
stmt.body.map((s) => statement(ctx, s)),
|
|
245
|
+
);
|
|
246
|
+
return js`for (let ${b} = ${num(stmt.start ?? 0)}; ${b} < ${expr(ctx, stmt.target)}.length; ${b}++) { ${body} }`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A keyword failure: an error unit in list mode, fail-fast in flag mode. */
|
|
250
|
+
function emitFail(
|
|
251
|
+
ctx: UnitContext,
|
|
252
|
+
stmt: Extract<LowerStmt, { kind: "fail" }>,
|
|
253
|
+
): CodeChunk {
|
|
254
|
+
if (ctx.output === "list") {
|
|
255
|
+
return pushError(
|
|
256
|
+
ctx,
|
|
257
|
+
renderMessage(ctx, stmt.message),
|
|
258
|
+
true,
|
|
259
|
+
paramsChunk(ctx, stmt.params),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
// Flag mode: verdict-only, fail fast.
|
|
263
|
+
return js`return false;`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The keyword's own value as an annotation unit (annotation mode only). */
|
|
267
|
+
function emitAnnotate(
|
|
268
|
+
ctx: UnitContext,
|
|
269
|
+
_stmt: Extract<LowerStmt, { kind: "annotate" }>,
|
|
270
|
+
): CodeChunk {
|
|
271
|
+
// Annotation mode records the keyword's own value as a unit; every
|
|
272
|
+
// other mode elides. The value is a schema constant (draft-03 §12.9).
|
|
273
|
+
if (!ctx.annMode || !ctx.annKwKept) return js``;
|
|
274
|
+
const value = (ctx.unit.ref.node as Record<string, JsonValue>)[
|
|
275
|
+
ctx.currentKeyword
|
|
276
|
+
]!;
|
|
277
|
+
return annsPush(ctx, annUnit(ctx, json(value)));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A consumed producer's dependency data onto the coverage channel (region mode only). */
|
|
281
|
+
function emitProduce(
|
|
282
|
+
ctx: UnitContext,
|
|
283
|
+
stmt: Extract<LowerStmt, { kind: "produce" }>,
|
|
284
|
+
): CodeChunk {
|
|
285
|
+
// Region mode pushes a consumed producer's dependency data onto the
|
|
286
|
+
// channel (rule 5); dependency data is never an annotation unit. In
|
|
287
|
+
// list mode the push waits on the keyword's own verdict (kwOk).
|
|
288
|
+
if (!ctx.covKwKept) return js``;
|
|
289
|
+
const push = produceChannel(ctx, stmt.value);
|
|
290
|
+
return ctx.kwOk ? js`if (${ctx.kwOk}) { ${push} }` : push;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Bind the channel fold a following coverageCovers reads (region mode only). */
|
|
294
|
+
function emitCoverageFold(
|
|
295
|
+
ctx: UnitContext,
|
|
296
|
+
stmt: Extract<LowerStmt, { kind: "coverageFold" }>,
|
|
297
|
+
): CodeChunk {
|
|
298
|
+
// Bind the channel fold a following coverageCovers reads (rule 6). Only
|
|
299
|
+
// a tracked unit's consumer emits this, and only in region mode.
|
|
300
|
+
if (!ctx.regionMode) {
|
|
301
|
+
throw new SerializeError("coverageFold requires region emission (phase B)");
|
|
302
|
+
}
|
|
303
|
+
const f = foldVar(ctx.counters.temp++);
|
|
304
|
+
ctx.coverageFolds.set(stmt.binding, { var: f, half: stmt.half });
|
|
305
|
+
if (stmt.half === "names") {
|
|
306
|
+
return js`const ${f} = ${id("h_covN")}(${EV});`;
|
|
307
|
+
}
|
|
308
|
+
// indexes: fold over the current instance array's length (the enclosing
|
|
309
|
+
// lower() guards this with an array-type test).
|
|
310
|
+
return js`const ${f} = ${id("h_covI")}(${EV}, ${ctx.valueVar}.length);`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The contains shape: count matching items, then check the range; the probe's spans and the loop's error mark follow the mode. */
|
|
314
|
+
function emitCountRange(
|
|
315
|
+
ctx: UnitContext,
|
|
316
|
+
stmt: Extract<LowerStmt, { kind: "countRange" }>,
|
|
317
|
+
): CodeChunk {
|
|
318
|
+
const b = bindingVar(stmt.binding);
|
|
319
|
+
const c = counterVar(ctx.counters.tally++);
|
|
320
|
+
const max = Number.isFinite(stmt.max) ? num(stmt.max) : null;
|
|
321
|
+
const outOfRange =
|
|
322
|
+
max === null
|
|
323
|
+
? js`${c} < ${num(stmt.min)}`
|
|
324
|
+
: js`${c} < ${num(stmt.min)} || ${c} > ${max}`;
|
|
325
|
+
const onFail =
|
|
326
|
+
ctx.output === "list"
|
|
327
|
+
? pushError(
|
|
328
|
+
ctx,
|
|
329
|
+
renderMessage(ctx, stmt.outOfRangeMessage, c),
|
|
330
|
+
true,
|
|
331
|
+
paramsChunk(ctx, stmt.outOfRangeParams, c),
|
|
332
|
+
)
|
|
333
|
+
: js`return false;`;
|
|
334
|
+
// List mode: an accepting contains drops every probe's errors, a
|
|
335
|
+
// rejecting one keeps them (§12.2) — one mark around the whole loop,
|
|
336
|
+
// never per probe.
|
|
337
|
+
const em = errMark(ctx);
|
|
338
|
+
const errDecl = em ? js`const ${em} = ${errsLength(ctx)}; ` : js``;
|
|
339
|
+
const check = em
|
|
340
|
+
? js`if (${outOfRange}) { ${onFail} } else { ${errsCut(ctx, em)} }`
|
|
341
|
+
: js`if (${outOfRange}) { ${onFail} }`;
|
|
342
|
+
// contains' probe IS a bare applyExpr (the only shape besides `if`'s
|
|
343
|
+
// condition): mark/truncate each probe so a matching item's
|
|
344
|
+
// annotations merge and a failing item's discard. collectIndexes feeds
|
|
345
|
+
// the matched-index accumulator the following produce renders.
|
|
346
|
+
if (ctx.annMode && stmt.countWhen.kind === "applyExpr") {
|
|
347
|
+
const probe = applyCall(ctx, stmt.countWhen.apply);
|
|
348
|
+
const m = id("m" + String(ctx.counters.temp++));
|
|
349
|
+
const pushIdx =
|
|
350
|
+
ctx.annKw?.matched && stmt.collectIndexes
|
|
351
|
+
? js` ${ctx.annKw.matched}.push(${b});`
|
|
352
|
+
: js``;
|
|
353
|
+
const loop = js`${errDecl}let ${c} = 0; for (let ${b} = 0; ${b} < ${expr(ctx, stmt.target)}.length; ${b}++) { const ${m} = ${annsLength(ctx)}; if (${probe}) { ${c}++;${pushIdx} } else ${annsCut(ctx, m)} }`;
|
|
354
|
+
return js`${loop} ${check}`;
|
|
355
|
+
}
|
|
356
|
+
if (ctx.regionMode && stmt.countWhen.kind === "applyExpr") {
|
|
357
|
+
// The probe is a CHILD-cursor apply (a plain call that never receives
|
|
358
|
+
// `ev`), so there is no channel span to mark here — `contains`'
|
|
359
|
+
// coverage reaches the channel only through its matched-index produce
|
|
360
|
+
// (rule 5). Accumulate the matched indexes for that produce.
|
|
361
|
+
const probe = applyCall(ctx, stmt.countWhen.apply);
|
|
362
|
+
const pushIdx =
|
|
363
|
+
ctx.annKw?.matched && stmt.collectIndexes
|
|
364
|
+
? js` ${ctx.annKw.matched}.push(${b});`
|
|
365
|
+
: js``;
|
|
366
|
+
const loop = js`${errDecl}let ${c} = 0; for (let ${b} = 0; ${b} < ${expr(ctx, stmt.target)}.length; ${b}++) { if (${probe}) { ${c}++;${pushIdx} } }`;
|
|
367
|
+
return js`${loop} ${check}`;
|
|
368
|
+
}
|
|
369
|
+
const loop = js`${errDecl}let ${c} = 0; for (let ${b} = 0; ${b} < ${expr(ctx, stmt.target)}.length; ${b}++) { if (${expr(ctx, stmt.countWhen)}) ${c}++; }`;
|
|
370
|
+
return js`${loop} ${check}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function statement(ctx: UnitContext, stmt: LowerStmt): CodeChunk {
|
|
374
|
+
switch (stmt.kind) {
|
|
375
|
+
case "if":
|
|
376
|
+
return emitIf(ctx, stmt);
|
|
377
|
+
case "forEachKey":
|
|
378
|
+
return emitForEachKey(ctx, stmt);
|
|
379
|
+
case "forEachIndex":
|
|
380
|
+
return emitForEachIndex(ctx, stmt);
|
|
381
|
+
case "fail":
|
|
382
|
+
return emitFail(ctx, stmt);
|
|
383
|
+
case "annotate":
|
|
384
|
+
return emitAnnotate(ctx, stmt);
|
|
385
|
+
case "produce":
|
|
386
|
+
return emitProduce(ctx, stmt);
|
|
387
|
+
case "coverageFold":
|
|
388
|
+
return emitCoverageFold(ctx, stmt);
|
|
389
|
+
case "apply":
|
|
390
|
+
return applyStatement(ctx, stmt.apply);
|
|
391
|
+
case "combineCheck":
|
|
392
|
+
throw new SerializeError(
|
|
393
|
+
"combineCheck must directly follow its anyMayPass/exactlyOne run",
|
|
394
|
+
);
|
|
395
|
+
case "countRange":
|
|
396
|
+
return emitCountRange(ctx, stmt);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** List-mode conjunct and negation folds (null for the grouped folds). */
|
|
401
|
+
function emitListApply(
|
|
402
|
+
ctx: UnitContext,
|
|
403
|
+
apply: LowerApply,
|
|
404
|
+
call: CodeChunk,
|
|
405
|
+
evSpan: boolean,
|
|
406
|
+
): CodeChunk | null {
|
|
407
|
+
switch (apply.fold) {
|
|
408
|
+
case "allMustPass": {
|
|
409
|
+
// Record the attempted segment (before the verdict) for the
|
|
410
|
+
// keyword's produce accumulator, then mark the channel spans so a
|
|
411
|
+
// failing application's annotations (annMode) and coverage (region
|
|
412
|
+
// emission) truncate — its errors stay (the keyword rejects), and
|
|
413
|
+
// list mode CONTINUES on failure (ok = false, no return): the
|
|
414
|
+
// unit's remaining keywords, including a consumer sweep that now
|
|
415
|
+
// covers less, still run.
|
|
416
|
+
const rec = annRecordSegment(ctx, apply.cursor);
|
|
417
|
+
const pre = rec ? js`${rec} ` : js``;
|
|
418
|
+
// A driven sibling's application (`then`/`else` under `if`) fails that
|
|
419
|
+
// sibling's own verdict, as the interpreter reports it.
|
|
420
|
+
const slot =
|
|
421
|
+
ctx.trace && apply.sibling !== undefined
|
|
422
|
+
? (ctx.kwVerdicts.get(apply.sibling) ?? null)
|
|
423
|
+
: ctx.kwOk;
|
|
424
|
+
const fail = slot ? js`ok = false; ${slot} = false;` : js`ok = false;`;
|
|
425
|
+
const spans = channelSpans(ctx, evSpan);
|
|
426
|
+
if (spans.length === 0) return js`${pre}if (!${call}) { ${fail} }`;
|
|
427
|
+
return js`${pre}${spanDecls(ctx, spans)} if (!${call}) { ${fail} ${spanResets(ctx, spans)} }`;
|
|
428
|
+
}
|
|
429
|
+
case "negate": {
|
|
430
|
+
// A failing negated subschema's spans truncate, and so do its
|
|
431
|
+
// errors (`not` accepts, §12.2); a PASSING one's annotations and
|
|
432
|
+
// coverage STAY (the interpreter merges any passing application
|
|
433
|
+
// into the unit frame, and same-unit consumers see it) while the
|
|
434
|
+
// unit records the not-error.
|
|
435
|
+
const spans = channelSpans(ctx, evSpan);
|
|
436
|
+
const em = errMark(ctx)!;
|
|
437
|
+
const err = pushError(
|
|
438
|
+
ctx,
|
|
439
|
+
renderMessage(ctx, apply.message ?? ["must not match the subschema"]),
|
|
440
|
+
true,
|
|
441
|
+
paramsChunk(ctx, apply.params),
|
|
442
|
+
);
|
|
443
|
+
return js`${spanDecls(ctx, spans)} const ${em} = ${errsLength(ctx)}; if (${call}) { ${err} } else { ${errsCut(ctx, em)} ${spanResets(ctx, spans)} }`;
|
|
444
|
+
}
|
|
445
|
+
default:
|
|
446
|
+
return null; // grouped folds handled by keywordStatements; discard by applyExpr
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Region-mode conjunct and negation folds (null for the grouped folds). */
|
|
451
|
+
function emitRegionApply(
|
|
452
|
+
ctx: UnitContext,
|
|
453
|
+
apply: LowerApply,
|
|
454
|
+
call: CodeChunk,
|
|
455
|
+
evSpan: boolean,
|
|
456
|
+
): CodeChunk | null {
|
|
457
|
+
switch (apply.fold) {
|
|
458
|
+
case "allMustPass": {
|
|
459
|
+
// Record the attempted child segment (before the verdict) for a
|
|
460
|
+
// consumed producer's channel push (rule 5).
|
|
461
|
+
const rec = annRecordSegment(ctx, apply.cursor);
|
|
462
|
+
const pre = rec ? js`${rec} ` : js``;
|
|
463
|
+
if (call.text === "true") return rec ?? js``;
|
|
464
|
+
if (call.text === "false") return js`${pre}return false;`;
|
|
465
|
+
if (evSpan) {
|
|
466
|
+
// In-place conjunct: a failed span truncates and fails the unit;
|
|
467
|
+
// the caller's own span then truncates this whole call (rule 2).
|
|
468
|
+
const m = id("m" + String(ctx.counters.temp++));
|
|
469
|
+
return js`const ${m} = ${EV}.length; if (!${call}) { ${EV}.length = ${m}; return false; }`;
|
|
470
|
+
}
|
|
471
|
+
// Child-cursor conjunct (a swept property/item): no channel span.
|
|
472
|
+
return js`${pre}if (!${call}) return false;`;
|
|
473
|
+
}
|
|
474
|
+
case "negate": {
|
|
475
|
+
if (call.text === "true") return js`return false;`;
|
|
476
|
+
if (call.text === "false") return js``;
|
|
477
|
+
// A passing negated subschema dies with the unit (return false → the
|
|
478
|
+
// caller truncates); a failing one truncates its own span (rule 3).
|
|
479
|
+
const m = id("m" + String(ctx.counters.temp++));
|
|
480
|
+
return js`const ${m} = ${EV}.length; if (${call}) return false; ${EV}.length = ${m};`;
|
|
481
|
+
}
|
|
482
|
+
default:
|
|
483
|
+
return null; // grouped folds handled by keywordStatements; discard by applyExpr
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function applyStatement(ctx: UnitContext, apply: LowerApply): CodeChunk {
|
|
488
|
+
if (apply.fold === "allMustPass") {
|
|
489
|
+
const inlined = tryInline(ctx, apply);
|
|
490
|
+
if (inlined) return inlined;
|
|
491
|
+
}
|
|
492
|
+
const call = applyCall(ctx, apply);
|
|
493
|
+
// A call RECEIVES the coverage channel exactly when it is an in-place
|
|
494
|
+
// apply to a non-boolean target (a region variant, or a coverage-
|
|
495
|
+
// harvesting trampoline); a child-cursor apply is a plain call, and a
|
|
496
|
+
// boolean folds to a literal or reports without producing — neither
|
|
497
|
+
// touches the channel, so neither needs an ev mark/truncate span (rule 1).
|
|
498
|
+
const evSpan =
|
|
499
|
+
ctx.regionMode &&
|
|
500
|
+
apply.cursor.kind === "here" &&
|
|
501
|
+
call.text !== "true" &&
|
|
502
|
+
call.text !== "false";
|
|
503
|
+
if (ctx.output === "list") {
|
|
504
|
+
const folded = emitListApply(ctx, apply, call, evSpan);
|
|
505
|
+
if (folded) return folded;
|
|
506
|
+
} else if (ctx.regionMode) {
|
|
507
|
+
const folded = emitRegionApply(ctx, apply, call, evSpan);
|
|
508
|
+
if (folded) return folded;
|
|
509
|
+
}
|
|
510
|
+
if (apply.fold === "allMustPass" && call.text === "true") return js``;
|
|
511
|
+
if (apply.fold === "allMustPass" && call.text === "false")
|
|
512
|
+
return js`return false;`;
|
|
513
|
+
switch (apply.fold) {
|
|
514
|
+
case "allMustPass":
|
|
515
|
+
return js`if (!${call}) return false;`;
|
|
516
|
+
case "negate":
|
|
517
|
+
return js`if (${call}) return false;`;
|
|
518
|
+
case "anyMayPass":
|
|
519
|
+
case "exactlyOne":
|
|
520
|
+
// keywordStatements groups consecutive runs of these before they
|
|
521
|
+
// reach here (a lone run of one is still a "run").
|
|
522
|
+
throw new SerializeError(
|
|
523
|
+
"fold '" + apply.fold + "' must be grouped by keywordStatements",
|
|
524
|
+
);
|
|
525
|
+
case "discard":
|
|
526
|
+
// A bare discard apply statement has no verdict-folding meaning —
|
|
527
|
+
// it's only legal as an applyExpr operand (the if/contains probe
|
|
528
|
+
// shape), never a standalone statement.
|
|
529
|
+
throw new SerializeError("fold 'discard' is only legal inside applyExpr");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Per-unit orchestration: run each present keyword's lower() in dialect
|
|
2
|
+
// order and hand its statements to the emitters.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
escapeSegment,
|
|
6
|
+
type JsonValue,
|
|
7
|
+
type LowerStmt,
|
|
8
|
+
type LoweringContext,
|
|
9
|
+
} from "@json-schema-engine/core";
|
|
10
|
+
import { type CodeChunk, id, join, js, str, json } from "../emit.js";
|
|
11
|
+
import { TN } from "./names.js";
|
|
12
|
+
import { UnitContext, SerializeError } from "./context.js";
|
|
13
|
+
import { beginKeyword } from "./keywords.js";
|
|
14
|
+
import { keywordStatements } from "./statements.js";
|
|
15
|
+
import { annsPush } from "./spans.js";
|
|
16
|
+
|
|
17
|
+
/** Serialize every present keyword of this unit, in dialect order. */
|
|
18
|
+
export function unitBody(ctx: UnitContext): CodeChunk[] {
|
|
19
|
+
const node = ctx.unit.ref.node as Record<string, JsonValue>;
|
|
20
|
+
const dialect = ctx.registry.dialectFor(ctx.unit.ref.baseUri);
|
|
21
|
+
// Mirrors buildPlan's refOnly (plan.ts, citing engine.ts:397): the plan
|
|
22
|
+
// only resolved edges for $ref when this is true, so siblings must be
|
|
23
|
+
// skipped here too, or this would try to serialize applies the plan
|
|
24
|
+
// never planned.
|
|
25
|
+
const refOnly = dialect.refIgnoresSiblings && Object.hasOwn(node, "$ref");
|
|
26
|
+
const out: CodeChunk[] = [];
|
|
27
|
+
if (ctx.trace) {
|
|
28
|
+
// One verdict slot per present non-structural keyword, declared up front:
|
|
29
|
+
// `if` settles `then`/`else`'s verdicts before those keywords' own (empty)
|
|
30
|
+
// statement lists run. Structural keywords record no entry (engine.ts).
|
|
31
|
+
const slots: CodeChunk[] = [];
|
|
32
|
+
for (const entry of dialect.ordered) {
|
|
33
|
+
if (refOnly && entry.name !== "$ref") continue;
|
|
34
|
+
if (!Object.hasOwn(node, entry.name)) continue;
|
|
35
|
+
if (entry.behavior.structural === true) continue;
|
|
36
|
+
const k = id("k" + String(ctx.counters.temp++));
|
|
37
|
+
ctx.kwVerdicts.set(entry.name, k);
|
|
38
|
+
slots.push(js`${k} = true`);
|
|
39
|
+
}
|
|
40
|
+
if (slots.length > 0) out.push(js`let ${join(", ", slots)};`);
|
|
41
|
+
}
|
|
42
|
+
for (const entry of dialect.ordered) {
|
|
43
|
+
if (refOnly && entry.name !== "$ref") continue;
|
|
44
|
+
if (!Object.hasOwn(node, entry.name)) continue;
|
|
45
|
+
const behavior = entry.behavior;
|
|
46
|
+
if (typeof behavior.lower !== "function") {
|
|
47
|
+
throw new SerializeError(
|
|
48
|
+
"planner accepted unlowerable keyword '" + entry.name + "' (bug)",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const stmts = collect(ctx, entry.name, (lctx) => {
|
|
52
|
+
behavior.lower!(node[entry.name]!, lctx);
|
|
53
|
+
});
|
|
54
|
+
ctx.currentVocab = entry.vocabularyUri;
|
|
55
|
+
ctx.currentBehaviorId = behavior.id;
|
|
56
|
+
// Accumulator declarations hoist above the keyword's statements: the
|
|
57
|
+
// application call sites feed them, the produce reads them.
|
|
58
|
+
const decls = beginKeyword(ctx, stmts);
|
|
59
|
+
const chunks = keywordStatements(ctx, stmts);
|
|
60
|
+
out.push(...decls, ...chunks);
|
|
61
|
+
// The verdict entry follows the keyword's applications, as the
|
|
62
|
+
// interpreter's traceKeyword does.
|
|
63
|
+
if (ctx.trace && behavior.structural !== true) {
|
|
64
|
+
const k = ctx.kwVerdicts.get(entry.name)!;
|
|
65
|
+
out.push(
|
|
66
|
+
js`${TN}.keywords.push({ name: ${str(entry.name)}, valid: ${k} });`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Unknown keywords collect as annotations (engine.ts:414): unconditional
|
|
71
|
+
// constant annotations, in schema-key order, after every dialect keyword.
|
|
72
|
+
// The refOnly break silences siblings, matching the interpreter. Trace
|
|
73
|
+
// emission records each as an accepting keyword entry whether or not its
|
|
74
|
+
// annotation is selected.
|
|
75
|
+
if ((ctx.annMode || ctx.trace) && !refOnly) {
|
|
76
|
+
for (const name of Object.keys(node)) {
|
|
77
|
+
if (dialect.keywords.has(name)) continue;
|
|
78
|
+
if (ctx.trace) {
|
|
79
|
+
out.push(js`${TN}.keywords.push({ name: ${str(name)}, valid: true });`);
|
|
80
|
+
}
|
|
81
|
+
if (!ctx.annMode) continue;
|
|
82
|
+
if (ctx.annKeep && !ctx.annKeep("", name, null)) continue;
|
|
83
|
+
const suffix = "/" + escapeSegment(name);
|
|
84
|
+
const sloc = ctx.unit.ref.baseUri + "#" + ctx.unit.ref.pointer + suffix;
|
|
85
|
+
out.push(
|
|
86
|
+
annsPush(
|
|
87
|
+
ctx,
|
|
88
|
+
js`{ keyword: ${str(name)}, evaluationPath: ${id("ep")} + ${str(suffix)}, schemaLocation: ${str(sloc)}, inputLocation: ${id("ip")}, annotation: ${json(node[name]!)} }`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Run one keyword's lower() against a fresh LoweringContext, return its stmts. */
|
|
97
|
+
export function collect(
|
|
98
|
+
ctx: UnitContext,
|
|
99
|
+
keyword: string,
|
|
100
|
+
run: (lctx: LoweringContext) => void,
|
|
101
|
+
): LowerStmt[] {
|
|
102
|
+
ctx.currentKeyword = keyword;
|
|
103
|
+
const stmts: LowerStmt[] = [];
|
|
104
|
+
const unit = ctx.unit;
|
|
105
|
+
const lctx: LoweringContext = {
|
|
106
|
+
instance: { kind: "instance" },
|
|
107
|
+
schema: unit.ref.node as Record<string, JsonValue>,
|
|
108
|
+
staticCoverage: () => unit.coverage,
|
|
109
|
+
// A tracked unit's consumers read the runtime channel; a static-coverage
|
|
110
|
+
// consumer (even one that is a region member) keeps its static path and
|
|
111
|
+
// instead PUSHES its produce onto the channel for the enclosing consumer.
|
|
112
|
+
runtimeCoverage: () => unit.tracking === true,
|
|
113
|
+
emit: (...s) => stmts.push(...s),
|
|
114
|
+
binding: () => ctx.counters.binding++,
|
|
115
|
+
};
|
|
116
|
+
run(lctx);
|
|
117
|
+
return stmts;
|
|
118
|
+
}
|