@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,432 @@
|
|
|
1
|
+
// IR serializer (M6.2): planned units + keyword lower() IR → artifact
|
|
2
|
+
// source. Flag-mode semantics: fail-fast within a unit (verdict-only),
|
|
3
|
+
// annotations elided, anyOf short-circuit licensed because the planner
|
|
4
|
+
// interprets any node whose channel could be observed (slice licensing;
|
|
5
|
+
// DESIGN §7). All text assembly goes through the gated formatter (emit.ts).
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
makeRecordPredicate,
|
|
9
|
+
type RecordPredicate,
|
|
10
|
+
type SchemaRegistry,
|
|
11
|
+
} from "@json-schema-engine/core";
|
|
12
|
+
import { type CodeChunk, frag, id, join, js, num, raw, str } from "../emit.js";
|
|
13
|
+
import type { CompilationPlan, PlannedUnit } from "../plan.js";
|
|
14
|
+
import {
|
|
15
|
+
V,
|
|
16
|
+
D,
|
|
17
|
+
S,
|
|
18
|
+
R,
|
|
19
|
+
T,
|
|
20
|
+
EV,
|
|
21
|
+
EP,
|
|
22
|
+
IP,
|
|
23
|
+
ST,
|
|
24
|
+
TP,
|
|
25
|
+
TN,
|
|
26
|
+
type ChannelShape,
|
|
27
|
+
unitFn,
|
|
28
|
+
unitFnRegion,
|
|
29
|
+
regexConst,
|
|
30
|
+
formatConst,
|
|
31
|
+
channelArgs,
|
|
32
|
+
fragHelper,
|
|
33
|
+
} from "./names.js";
|
|
34
|
+
import {
|
|
35
|
+
UnitContext,
|
|
36
|
+
SerializeError,
|
|
37
|
+
type EmitMode,
|
|
38
|
+
type EmitFlags,
|
|
39
|
+
DEFAULT_FLAGS,
|
|
40
|
+
type SerializeOptions,
|
|
41
|
+
} from "./context.js";
|
|
42
|
+
import { guardDecl } from "./guards.js";
|
|
43
|
+
import { unitBody } from "./unit.js";
|
|
44
|
+
|
|
45
|
+
export type {
|
|
46
|
+
AnnotateOptions,
|
|
47
|
+
EmitFlags,
|
|
48
|
+
EmitMode,
|
|
49
|
+
EmitOutput,
|
|
50
|
+
SerializeOptions,
|
|
51
|
+
} from "./context.js";
|
|
52
|
+
|
|
53
|
+
/** The root application call the footer wraps: static function or trampoline, per mode. */
|
|
54
|
+
function rootCallChunk(
|
|
55
|
+
plan: CompilationPlan,
|
|
56
|
+
fnIndex: Map<string, number>,
|
|
57
|
+
tableIndex: Map<string, number>,
|
|
58
|
+
shape: ChannelShape,
|
|
59
|
+
): CodeChunk {
|
|
60
|
+
const root = plan.units.get(plan.rootKey)!;
|
|
61
|
+
// The root's prefixes are empty; under trace emission its parent is the
|
|
62
|
+
// state's holder node.
|
|
63
|
+
const args = channelArgs(
|
|
64
|
+
shape,
|
|
65
|
+
() => [str(""), str("")],
|
|
66
|
+
js`${ST}.hold`,
|
|
67
|
+
false,
|
|
68
|
+
);
|
|
69
|
+
if (root.kind === "static") {
|
|
70
|
+
const fn = unitFn(fnIndex.get(root.key)!);
|
|
71
|
+
return js`${fn}(${join(", ", [V, num(0), id("h_s0"), ...args])})`;
|
|
72
|
+
}
|
|
73
|
+
const slot = num(tableIndex.get(root.key)!);
|
|
74
|
+
const helper = fragHelper(shape, false);
|
|
75
|
+
return js`${helper}(${join(", ", [js`${T}[${slot}]`, V, id("h_s0"), num(0), ...args])})`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Helper bindings, the depth bound, and the hoisted regex/format consts (D9f). */
|
|
79
|
+
function prologueChunks(
|
|
80
|
+
plan: CompilationPlan,
|
|
81
|
+
mode: EmitMode,
|
|
82
|
+
shape: ChannelShape,
|
|
83
|
+
hasRegion: boolean,
|
|
84
|
+
): CodeChunk[] {
|
|
85
|
+
const { output, annMode, trace } = shape;
|
|
86
|
+
// Prologue hoists (D9f): helper bindings, the depth bound, and one const
|
|
87
|
+
// per regex source — property/table lookups move out of the hot path.
|
|
88
|
+
// Standalone mode: the module preamble (standalone.ts) already defines the
|
|
89
|
+
// h_-named helpers; only the regex consts are emitted here, built through
|
|
90
|
+
// the preamble's u-flag-with-fallback constructor.
|
|
91
|
+
const prologue: CodeChunk[] = [];
|
|
92
|
+
if (mode === "runtime") {
|
|
93
|
+
prologue.push(
|
|
94
|
+
js`const { isObject: h_obj, isInteger: h_int, jsonEqual: h_eq, canonicalKey: h_ck, codePointLength: h_cpl, escapeSegment: h_esc, isMultipleOf: h_mof, hasDuplicateItems: h_dup, firstDuplicatePair: h_fdp, frag: h_frag, fragList: h_fragl, tooDeep: h_deep } = ${R};`,
|
|
95
|
+
js`const h_maxd = ${R}.maxDepth;`,
|
|
96
|
+
// Shared empty dynamic scope: units append-by-copy, never mutate.
|
|
97
|
+
js`const h_s0 = [];`,
|
|
98
|
+
js`const h_hop = Object.prototype.hasOwnProperty;`,
|
|
99
|
+
);
|
|
100
|
+
if (annMode && !trace) {
|
|
101
|
+
prologue.push(js`const h_fragla = ${R}.fragListAnn;`);
|
|
102
|
+
}
|
|
103
|
+
// Trace emission records through the runtime: node creation, the record
|
|
104
|
+
// pushes that name their application, the level-dependent cuts, and the
|
|
105
|
+
// traced island trampoline.
|
|
106
|
+
if (trace) {
|
|
107
|
+
prologue.push(
|
|
108
|
+
js`const { traceState: h_tstate, traceNode: h_tnode, traceError: h_err, traceAnn: h_ann, cutErrors: h_cutE, cutAnns: h_cutA, fragTrace: h_fragt } = ${R};`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
// Region emission (phase B) helpers: the two channel folds, plus the
|
|
112
|
+
// coverage-harvesting island trampoline in flag mode (list islands go
|
|
113
|
+
// through the list trampolines' trailing-`ev` overloads instead). Only
|
|
114
|
+
// bound when a tracked/region unit exists, so consumer-free artifacts
|
|
115
|
+
// keep their prologue unchanged.
|
|
116
|
+
if (hasRegion) {
|
|
117
|
+
prologue.push(
|
|
118
|
+
output === "flag"
|
|
119
|
+
? js`const h_covN = ${R}.foldNameCoverage, h_covI = ${R}.foldIndexCoverage, h_fragc = ${R}.fragCov;`
|
|
120
|
+
: js`const h_covN = ${R}.foldNameCoverage, h_covI = ${R}.foldIndexCoverage;`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
plan.patterns.forEach((source, i) => {
|
|
125
|
+
prologue.push(
|
|
126
|
+
mode === "runtime"
|
|
127
|
+
? js`const ${regexConst(i)} = ${R}.re[${str(source)}];`
|
|
128
|
+
: js`const ${regexConst(i)} = ${id("h_rx")}(${str(source)});`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
// One format-definition lookup per used name (runtime mode only). Standalone
|
|
132
|
+
// never reaches a format-bearing plan — emitStandalone rejects plan.formats
|
|
133
|
+
// (a format predicate like IDNA cannot be duplicated into a zero-import
|
|
134
|
+
// module), so plan.formats is empty here in that mode.
|
|
135
|
+
plan.formats.forEach((name, i) => {
|
|
136
|
+
prologue.push(js`const ${formatConst(i)} = ${R}.formats[${str(name)}];`);
|
|
137
|
+
});
|
|
138
|
+
return prologue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The artifact's entry function, per output and mode. */
|
|
142
|
+
function footerChunk(
|
|
143
|
+
mode: EmitMode,
|
|
144
|
+
shape: ChannelShape,
|
|
145
|
+
rootCall: CodeChunk,
|
|
146
|
+
): CodeChunk {
|
|
147
|
+
const { output, annMode, trace } = shape;
|
|
148
|
+
const ERRS = id("errs");
|
|
149
|
+
const ANNS = id("anns");
|
|
150
|
+
if (trace) {
|
|
151
|
+
// The state carries the flat channels with their owners and the tree;
|
|
152
|
+
// the artifact wrapper turns it into a Result.
|
|
153
|
+
return js`\nreturn function evaluateTrace(${V}) { const ${ST} = ${id("h_tstate")}(); const ok = ${rootCall}; ${ST}.valid = ok; return ${ST}; };\n`;
|
|
154
|
+
}
|
|
155
|
+
const footer = annMode
|
|
156
|
+
? js`\nreturn function evaluateList(${V}) { const ${ERRS} = []; const ${ANNS} = []; const ok = ${rootCall}; return { valid: ok, errors: ${ERRS}, annotations: ${ANNS} }; };\n`
|
|
157
|
+
: output === "list"
|
|
158
|
+
? js`\nreturn function evaluateList(${V}) { const ${ERRS} = []; const ok = ${rootCall}; return { valid: ok, errors: ${ERRS} }; };\n`
|
|
159
|
+
: mode === "runtime"
|
|
160
|
+
? js`\nreturn function validate(${V}) { return ${rootCall}; };\n`
|
|
161
|
+
: js`\nexport default function validate(${V}) { return ${rootCall}; };\n`;
|
|
162
|
+
return footer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Serializes one compilation plan into artifact source. */
|
|
166
|
+
export function serializePlan(
|
|
167
|
+
plan: CompilationPlan,
|
|
168
|
+
registry: SchemaRegistry,
|
|
169
|
+
options: SerializeOptions = {},
|
|
170
|
+
): string {
|
|
171
|
+
const {
|
|
172
|
+
mode = "runtime",
|
|
173
|
+
flags = DEFAULT_FLAGS,
|
|
174
|
+
output = "flag",
|
|
175
|
+
listParams = false,
|
|
176
|
+
annotate,
|
|
177
|
+
trace = false,
|
|
178
|
+
} = options;
|
|
179
|
+
if (output === "list" && mode === "standalone") {
|
|
180
|
+
throw new SerializeError("standalone emission is flag-only (M6.5 scope)");
|
|
181
|
+
}
|
|
182
|
+
if (trace && output !== "list") {
|
|
183
|
+
throw new SerializeError("trace emission is a list-mode variant");
|
|
184
|
+
}
|
|
185
|
+
// Runtime coverage tracking (COMPILED-CONSUMERS.md) composes with flag AND
|
|
186
|
+
// list/annotation outputs — list plans track every consumer (plan.ts), so
|
|
187
|
+
// region emission there is the normal case. Standalone stays out of scope.
|
|
188
|
+
const hasRegion = [...plan.units.values()].some(
|
|
189
|
+
(u) => u.tracking === true || u.inRegion === true,
|
|
190
|
+
);
|
|
191
|
+
if (hasRegion && mode === "standalone") {
|
|
192
|
+
throw new SerializeError(
|
|
193
|
+
"standalone emission does not support runtime coverage tracking (phase B)",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
// The coverage producers a consumer observes: a region producer pushes its
|
|
197
|
+
// raw dependency data onto the channel only when a consumer reads it and
|
|
198
|
+
// the data is coverage-shaped (rule 5; SchemaRegistry.coverageIds).
|
|
199
|
+
const coverageIds = registry.coverageIds();
|
|
200
|
+
// Annotation collection is a list-mode variant: it reuses the list plan and
|
|
201
|
+
// fail-open, no-short-circuit discipline, adding a flat `anns` channel with
|
|
202
|
+
// mark/truncate at every application boundary (channel rule 3).
|
|
203
|
+
const annMode = annotate !== undefined && output === "list";
|
|
204
|
+
const shape: ChannelShape = { output, annMode, trace };
|
|
205
|
+
// Static selection: the annotate/unknown-keyword allow/deny decision, applied
|
|
206
|
+
// at emit time so ruled-out annotations never emit. `keep` is deferred.
|
|
207
|
+
const annKeep: RecordPredicate | null = annMode
|
|
208
|
+
? makeRecordPredicate(annotate.selection ?? true)
|
|
209
|
+
: null;
|
|
210
|
+
// List mode disables inlining and boolean-literal folding: shared units
|
|
211
|
+
// carry the evaluation-path/instance-pointer parameters, and a `false`
|
|
212
|
+
// subschema must report "schema is false" rather than fold away.
|
|
213
|
+
const effFlags: EmitFlags =
|
|
214
|
+
output === "list" ? { ...flags, inline: false } : flags;
|
|
215
|
+
// Assign function indexes to static units, table slots to interpreted.
|
|
216
|
+
const fnIndex = new Map<string, number>();
|
|
217
|
+
const tableIndex = new Map<string, number>();
|
|
218
|
+
let nextFn = 0;
|
|
219
|
+
for (const unit of plan.units.values()) {
|
|
220
|
+
if (unit.kind === "static") fnIndex.set(unit.key, nextFn++);
|
|
221
|
+
}
|
|
222
|
+
plan.targets.forEach((u, i) => tableIndex.set(u.key, i));
|
|
223
|
+
|
|
224
|
+
const rendered: {
|
|
225
|
+
key: string;
|
|
226
|
+
boolean: boolean;
|
|
227
|
+
chunk: CodeChunk;
|
|
228
|
+
inlined: ReadonlySet<string>;
|
|
229
|
+
/** a channel-threaded region variant (never a dead-function candidate) */
|
|
230
|
+
region: boolean;
|
|
231
|
+
}[] = [];
|
|
232
|
+
for (const unit of plan.units.values()) {
|
|
233
|
+
if (unit.kind !== "static") continue;
|
|
234
|
+
rendered.push(
|
|
235
|
+
serializeUnit(
|
|
236
|
+
unit,
|
|
237
|
+
plan,
|
|
238
|
+
registry,
|
|
239
|
+
fnIndex,
|
|
240
|
+
tableIndex,
|
|
241
|
+
effFlags,
|
|
242
|
+
shape,
|
|
243
|
+
listParams,
|
|
244
|
+
annKeep,
|
|
245
|
+
coverageIds,
|
|
246
|
+
false,
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
// A region member carries a second emission whose signature takes the
|
|
250
|
+
// coverage channel; the plain variant above still serves child-cursor
|
|
251
|
+
// and non-region callers (phase B).
|
|
252
|
+
if (unit.inRegion) {
|
|
253
|
+
rendered.push(
|
|
254
|
+
serializeUnit(
|
|
255
|
+
unit,
|
|
256
|
+
plan,
|
|
257
|
+
registry,
|
|
258
|
+
fnIndex,
|
|
259
|
+
tableIndex,
|
|
260
|
+
effFlags,
|
|
261
|
+
shape,
|
|
262
|
+
listParams,
|
|
263
|
+
annKeep,
|
|
264
|
+
coverageIds,
|
|
265
|
+
true,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Drop dead functions: units expanded into their caller (D9c) and boolean
|
|
271
|
+
// units (their applications folded to literals). The root always stays.
|
|
272
|
+
// Region variants are always retained (correctness first; an uncalled one is
|
|
273
|
+
// inert declaration bytes, phase B size note).
|
|
274
|
+
const inlinedEverywhere = new Set<string>();
|
|
275
|
+
for (const r of rendered) for (const k of r.inlined) inlinedEverywhere.add(k);
|
|
276
|
+
// List mode calls boolean-false units (they report "schema is false"),
|
|
277
|
+
// so their functions survive the dead-function filter there.
|
|
278
|
+
const functions = rendered
|
|
279
|
+
.filter(
|
|
280
|
+
(r) =>
|
|
281
|
+
r.region ||
|
|
282
|
+
r.key === plan.rootKey ||
|
|
283
|
+
(!inlinedEverywhere.has(r.key) && (!r.boolean || output === "list")),
|
|
284
|
+
)
|
|
285
|
+
.map((r) => r.chunk);
|
|
286
|
+
|
|
287
|
+
const rootCall = rootCallChunk(plan, fnIndex, tableIndex, shape);
|
|
288
|
+
const prologue = prologueChunks(plan, mode, shape, hasRegion);
|
|
289
|
+
const footer = footerChunk(mode, shape, rootCall);
|
|
290
|
+
return frag(
|
|
291
|
+
raw(mode === "runtime" ? '"use strict";\n' : ""),
|
|
292
|
+
join("\n", prologue),
|
|
293
|
+
raw("\n"),
|
|
294
|
+
join("\n", functions),
|
|
295
|
+
footer,
|
|
296
|
+
).text;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function serializeUnit(
|
|
300
|
+
unit: PlannedUnit,
|
|
301
|
+
plan: CompilationPlan,
|
|
302
|
+
registry: SchemaRegistry,
|
|
303
|
+
fnIndex: Map<string, number>,
|
|
304
|
+
tableIndex: Map<string, number>,
|
|
305
|
+
flags: EmitFlags,
|
|
306
|
+
shape: ChannelShape,
|
|
307
|
+
listParams: boolean,
|
|
308
|
+
annKeep: RecordPredicate | null,
|
|
309
|
+
coverageIds: ReadonlySet<string>,
|
|
310
|
+
/** emitting the channel-threaded region variant of an inRegion unit */
|
|
311
|
+
regionVariant: boolean,
|
|
312
|
+
): {
|
|
313
|
+
key: string;
|
|
314
|
+
boolean: boolean;
|
|
315
|
+
chunk: CodeChunk;
|
|
316
|
+
inlined: ReadonlySet<string>;
|
|
317
|
+
region: boolean;
|
|
318
|
+
} {
|
|
319
|
+
// Region emission (phase B) applies to a tracked unit's body (a local
|
|
320
|
+
// channel `const ev = []`) and to an inRegion unit's region variant (the
|
|
321
|
+
// channel is a trailing parameter). A tracked unit is never inRegion (nested
|
|
322
|
+
// tracked consumers island), so the two never coincide.
|
|
323
|
+
const regionMode = regionVariant || unit.tracking === true;
|
|
324
|
+
if (regionVariant && unit.tracking) {
|
|
325
|
+
throw new SerializeError("a tracked unit cannot also be a region member");
|
|
326
|
+
}
|
|
327
|
+
const fn = regionVariant
|
|
328
|
+
? unitFnRegion(fnIndex.get(unit.key)!)
|
|
329
|
+
: unitFn(fnIndex.get(unit.key)!);
|
|
330
|
+
const node = unit.ref.node;
|
|
331
|
+
const { output, annMode, trace } = shape;
|
|
332
|
+
const sloc = str(unit.ref.baseUri + "#" + unit.ref.pointer);
|
|
333
|
+
// Trace emission: every application, boolean schemas included, is a node.
|
|
334
|
+
const enterNode = js`const ${TN} = ${id("h_tnode")}(${TP}, ${EP}, ${sloc}, ${IP});`;
|
|
335
|
+
// The unit signature carries the channels of the emission (channelArgs); a
|
|
336
|
+
// region variant appends the coverage channel after every other parameter.
|
|
337
|
+
const sig = js`(${join(", ", [V, D, S, ...channelArgs(shape, () => [EP, IP], TP, regionVariant)])})`;
|
|
338
|
+
|
|
339
|
+
if (typeof node === "boolean") {
|
|
340
|
+
// List mode: `false` reports the interpreter's boolean-schema error
|
|
341
|
+
// (keywordName null — no keyword suffix on either location).
|
|
342
|
+
// Structured-params mode: keywordName is null here, so the unit gets
|
|
343
|
+
// empty params and no keyword field (renderError's includeParams shape).
|
|
344
|
+
// A `false` schema never produces an annotation; the `anns` parameter is
|
|
345
|
+
// carried only to match the call signature.
|
|
346
|
+
const falseParams = listParams ? js`, params: {}` : js``;
|
|
347
|
+
const falseUnit = js`{ evaluationPath: ${id("ep")}, schemaLocation: ${sloc}, inputLocation: ${id("ip")}, error: "schema is false"${falseParams} }`;
|
|
348
|
+
const chunk = trace
|
|
349
|
+
? node
|
|
350
|
+
? js`function ${fn}${sig} { ${id("h_tnode")}(${TP}, ${EP}, ${sloc}, ${IP}); return true; }`
|
|
351
|
+
: js`function ${fn}${sig} { ${enterNode} ${TN}.valid = false; ${id("h_err")}(${ST}, ${TN}, ${falseUnit}); return false; }`
|
|
352
|
+
: output === "list" && !node
|
|
353
|
+
? js`function ${fn}${sig} { ${id("errs")}.push(${falseUnit}); return false; }`
|
|
354
|
+
: js`function ${fn}() { return ${raw(String(node))}; }`;
|
|
355
|
+
return {
|
|
356
|
+
key: unit.key,
|
|
357
|
+
boolean: true,
|
|
358
|
+
chunk,
|
|
359
|
+
inlined: new Set(),
|
|
360
|
+
region: false,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const body: CodeChunk[] = [];
|
|
365
|
+
if (unit.reachesInterpreted) {
|
|
366
|
+
// Dynamic-scope contribution: appended once per application, duplicates
|
|
367
|
+
// harmless (outermost-first resolution). Only threaded where a fragment
|
|
368
|
+
// can consume it.
|
|
369
|
+
body.push(js`${S} = [...${S}, ${str(unit.ref.baseUri)}];`);
|
|
370
|
+
}
|
|
371
|
+
if (trace) body.push(enterNode);
|
|
372
|
+
|
|
373
|
+
// A unit that participates in region emission (a tracked unit, or an
|
|
374
|
+
// inRegion unit — through EITHER variant) never inlines: a single-use child
|
|
375
|
+
// inlined into the plain variant would be dropped by the dead-function
|
|
376
|
+
// filter yet still called by the region variant (which does not inline).
|
|
377
|
+
const unitFlags =
|
|
378
|
+
regionMode || unit.inRegion === true ? { ...flags, inline: false } : flags;
|
|
379
|
+
const ctx = new UnitContext(
|
|
380
|
+
unit,
|
|
381
|
+
plan,
|
|
382
|
+
registry,
|
|
383
|
+
fnIndex,
|
|
384
|
+
tableIndex,
|
|
385
|
+
V,
|
|
386
|
+
{ binding: 0, tally: 0, temp: 0 },
|
|
387
|
+
new Set([unit.key]),
|
|
388
|
+
null,
|
|
389
|
+
unitFlags,
|
|
390
|
+
output,
|
|
391
|
+
listParams,
|
|
392
|
+
annMode,
|
|
393
|
+
annKeep,
|
|
394
|
+
regionMode,
|
|
395
|
+
coverageIds,
|
|
396
|
+
shape.trace,
|
|
397
|
+
);
|
|
398
|
+
const unitStmts = unitBody(ctx);
|
|
399
|
+
// Depth guard (D20 combined budget) only where a chain can grow: a
|
|
400
|
+
// function that calls no unit/fragment cannot recurse, and its own entry
|
|
401
|
+
// was budgeted by every caller on the way down.
|
|
402
|
+
if (ctx.calledUnit) {
|
|
403
|
+
body.unshift(js`if (${D} >= ${id("h_maxd")}) ${id("h_deep")}(); ${D}++;`);
|
|
404
|
+
}
|
|
405
|
+
const guard = guardDecl(ctx);
|
|
406
|
+
if (guard) body.push(guard);
|
|
407
|
+
if (output === "list") body.push(js`let ok = true;`);
|
|
408
|
+
// A tracked unit owns its channel locally (it is entered like any plain
|
|
409
|
+
// unit); a region variant receives the caller's channel as `ev`.
|
|
410
|
+
if (regionMode && !regionVariant) body.push(js`const ${EV} = [];`);
|
|
411
|
+
body.push(...unitStmts);
|
|
412
|
+
|
|
413
|
+
if (output === "list") {
|
|
414
|
+
if (trace) body.push(js`${TN}.valid = ok;`);
|
|
415
|
+
body.push(js`return ok;`);
|
|
416
|
+
return {
|
|
417
|
+
key: unit.key,
|
|
418
|
+
boolean: false,
|
|
419
|
+
chunk: js`function ${fn}${sig} { ${join("\n", body)} }`,
|
|
420
|
+
inlined: ctx.inlinedKeys,
|
|
421
|
+
region: regionVariant,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
body.push(js`return true;`);
|
|
425
|
+
return {
|
|
426
|
+
key: unit.key,
|
|
427
|
+
boolean: false,
|
|
428
|
+
chunk: js`function ${fn}${sig} { ${join("\n", body)} }`,
|
|
429
|
+
inlined: ctx.inlinedKeys,
|
|
430
|
+
region: regionVariant,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Per-keyword state in annotation and region modes: the produce accumulator
|
|
2
|
+
// both channels read, the annotation-unit literal, and the channel push.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
escapeSegment,
|
|
6
|
+
type LowerCursor,
|
|
7
|
+
type LowerProduceValue,
|
|
8
|
+
type LowerStmt,
|
|
9
|
+
} from "@json-schema-engine/core";
|
|
10
|
+
import { type CodeChunk, id, js, num, str } from "../emit.js";
|
|
11
|
+
import { EV, findProduce, hasAnnotate } from "./names.js";
|
|
12
|
+
import { UnitContext } from "./context.js";
|
|
13
|
+
import { expr } from "./expressions.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prepare the keyword currently being emitted: decide whether its annotate
|
|
17
|
+
* survives the static retention lists, whether its produce feeds the
|
|
18
|
+
* coverage channel, and allocate the produce's accumulator (declared by the
|
|
19
|
+
* returned chunks). No-op outside annotation and region modes.
|
|
20
|
+
*/
|
|
21
|
+
export function beginKeyword(
|
|
22
|
+
ctx: UnitContext,
|
|
23
|
+
stmts: readonly LowerStmt[],
|
|
24
|
+
): CodeChunk[] {
|
|
25
|
+
ctx.annKw = null;
|
|
26
|
+
ctx.annKwKept = false;
|
|
27
|
+
ctx.covKwKept = false;
|
|
28
|
+
// Trace emission pre-declared every keyword's verdict slot (unit.ts).
|
|
29
|
+
ctx.kwOk = ctx.trace
|
|
30
|
+
? (ctx.kwVerdicts.get(ctx.currentKeyword) ?? null)
|
|
31
|
+
: null;
|
|
32
|
+
if (!ctx.annMode && !ctx.regionMode) return [];
|
|
33
|
+
ctx.annKwKept =
|
|
34
|
+
ctx.annMode &&
|
|
35
|
+
hasAnnotate(stmts) &&
|
|
36
|
+
(!ctx.annKeep || ctx.annKeep("", ctx.currentKeyword, ctx.currentVocab));
|
|
37
|
+
const value = findProduce(stmts);
|
|
38
|
+
if (!value) return [];
|
|
39
|
+
// Region channel-push gate (rule 5): only a coverage producer some
|
|
40
|
+
// consumer reads feeds the channel. Retention never affects it —
|
|
41
|
+
// dependency data is not output.
|
|
42
|
+
ctx.covKwKept = ctx.regionMode && ctx.coverageIds.has(ctx.currentBehaviorId);
|
|
43
|
+
if (!ctx.covKwKept) return [];
|
|
44
|
+
const decls: CodeChunk[] = [];
|
|
45
|
+
if (ctx.output === "list" && !ctx.trace) {
|
|
46
|
+
const k = id("k" + String(ctx.counters.temp++));
|
|
47
|
+
ctx.kwOk = k;
|
|
48
|
+
decls.push(js`let ${k} = true;`);
|
|
49
|
+
}
|
|
50
|
+
if (value.kind === "collectedNames") {
|
|
51
|
+
const n = id("n" + String(ctx.counters.temp++));
|
|
52
|
+
ctx.annKw = { produceKind: "collectedNames", names: n };
|
|
53
|
+
decls.push(js`const ${n} = new Set();`);
|
|
54
|
+
} else if (value.render === "largestOrTrue") {
|
|
55
|
+
const m = id("n" + String(ctx.counters.temp++));
|
|
56
|
+
ctx.annKw = { produceKind: "largestOrTrue", max: m };
|
|
57
|
+
decls.push(js`let ${m} = -1;`);
|
|
58
|
+
} else if (value.render === "appliedTrue") {
|
|
59
|
+
const a = id("n" + String(ctx.counters.temp++));
|
|
60
|
+
ctx.annKw = { produceKind: "appliedTrue", applied: a };
|
|
61
|
+
decls.push(js`let ${a} = false;`);
|
|
62
|
+
} else {
|
|
63
|
+
const t = id("n" + String(ctx.counters.temp++));
|
|
64
|
+
ctx.annKw = { produceKind: "matchedOrAllTrue", matched: t };
|
|
65
|
+
decls.push(js`const ${t} = [];`);
|
|
66
|
+
}
|
|
67
|
+
return decls;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The annotation-unit object literal for the current keyword: constant
|
|
72
|
+
* keyword/vocabulary/schemaLocation, runtime evaluationPath (`ep` suffix)
|
|
73
|
+
* and inputLocation (`ip`), key order matching core's renderAnnotation.
|
|
74
|
+
* `vocabulary` is omitted when null (unknown keywords only).
|
|
75
|
+
*/
|
|
76
|
+
export function annUnit(ctx: UnitContext, valueExpr: CodeChunk): CodeChunk {
|
|
77
|
+
const suffix = "/" + escapeSegment(ctx.currentKeyword);
|
|
78
|
+
const sloc = ctx.unit.ref.baseUri + "#" + ctx.unit.ref.pointer + suffix;
|
|
79
|
+
const vocab =
|
|
80
|
+
ctx.currentVocab !== null
|
|
81
|
+
? js`vocabulary: ${str(ctx.currentVocab)}, `
|
|
82
|
+
: js``;
|
|
83
|
+
return js`{ keyword: ${str(ctx.currentKeyword)}, ${vocab}evaluationPath: ${id("ep")} + ${str(suffix)}, schemaLocation: ${str(sloc)}, inputLocation: ${id("ip")}, annotation: ${valueExpr} }`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Record an attempted child-of-here application's segment into the current
|
|
88
|
+
* keyword's produce accumulator (before the verdict — the segment is
|
|
89
|
+
* "attempted", not "succeeded"). Non-child-of-here cursors, and keywords
|
|
90
|
+
* without an active accumulator, record nothing.
|
|
91
|
+
*/
|
|
92
|
+
export function annRecordSegment(
|
|
93
|
+
ctx: UnitContext,
|
|
94
|
+
cursor: LowerCursor,
|
|
95
|
+
): CodeChunk | null {
|
|
96
|
+
if (!ctx.annKw) return null;
|
|
97
|
+
if (cursor.kind !== "child" || cursor.of.kind !== "here") return null;
|
|
98
|
+
const seg = cursor.segment;
|
|
99
|
+
const segExpr =
|
|
100
|
+
typeof seg === "string"
|
|
101
|
+
? str(seg)
|
|
102
|
+
: typeof seg === "number"
|
|
103
|
+
? num(seg)
|
|
104
|
+
: expr(ctx, seg);
|
|
105
|
+
switch (ctx.annKw.produceKind) {
|
|
106
|
+
case "collectedNames":
|
|
107
|
+
return js`${ctx.annKw.names!}.add(${segExpr});`;
|
|
108
|
+
case "largestOrTrue":
|
|
109
|
+
return js`if (${segExpr} > ${ctx.annKw.max!}) ${ctx.annKw.max!} = ${segExpr};`;
|
|
110
|
+
case "appliedTrue":
|
|
111
|
+
return js`${ctx.annKw.applied!} = true;`;
|
|
112
|
+
default:
|
|
113
|
+
// matchedOrAllTrue records inside its countRange, not at apply sites.
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Push a consumed producer's dependency data onto the runtime coverage
|
|
120
|
+
* channel (rule 5), writing the value the interpreter would produce. The
|
|
121
|
+
* "has data" guards match the interpreter's produce conditions so the
|
|
122
|
+
* channel carries exactly what a consumer's visible-records fold sees.
|
|
123
|
+
*/
|
|
124
|
+
export function produceChannel(
|
|
125
|
+
ctx: UnitContext,
|
|
126
|
+
value: LowerProduceValue,
|
|
127
|
+
): CodeChunk {
|
|
128
|
+
const v = ctx.valueVar;
|
|
129
|
+
if (value.kind === "collectedNames") {
|
|
130
|
+
// The enclosing lower() gates this produce behind an object-type test;
|
|
131
|
+
// the (attempted, deduped) name array spreads from the Set.
|
|
132
|
+
return js`${EV}.push([...${ctx.annKw!.names!}]);`;
|
|
133
|
+
}
|
|
134
|
+
if (value.render === "largestOrTrue") {
|
|
135
|
+
const mx = ctx.annKw!.max!;
|
|
136
|
+
return js`if (${mx} >= 0) ${EV}.push(${mx} + 1 === ${v}.length ? true : ${mx});`;
|
|
137
|
+
}
|
|
138
|
+
if (value.render === "appliedTrue") {
|
|
139
|
+
const ap = ctx.annKw!.applied!;
|
|
140
|
+
return js`if (${ap}) ${EV}.push(true);`;
|
|
141
|
+
}
|
|
142
|
+
const mt = ctx.annKw!.matched!;
|
|
143
|
+
return js`if (${mt}.length > 0) ${EV}.push(${mt}.length === ${v}.length ? true : ${mt});`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Error-unit rendering for list mode: messages, structured params, and the
|
|
2
|
+
// relevance mark that truncates a keyword's rejected sub-evaluation errors.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
escapeSegment,
|
|
6
|
+
type LowerMessage,
|
|
7
|
+
type LowerParams,
|
|
8
|
+
} from "@json-schema-engine/core";
|
|
9
|
+
import { type CodeChunk, id, join, js, str } from "../emit.js";
|
|
10
|
+
import { ERRS, ST, TN } from "./names.js";
|
|
11
|
+
import { UnitContext, SerializeError } from "./context.js";
|
|
12
|
+
import { expr } from "./expressions.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders a LowerMessage to a string expression. `tallyVar` binds the
|
|
16
|
+
* message's tally placeholder (combine/count checks).
|
|
17
|
+
*/
|
|
18
|
+
export function renderMessage(
|
|
19
|
+
ctx: UnitContext,
|
|
20
|
+
msg: LowerMessage,
|
|
21
|
+
tallyVar?: CodeChunk,
|
|
22
|
+
): CodeChunk {
|
|
23
|
+
const parts = msg.map((part) => {
|
|
24
|
+
if (typeof part === "string") return str(part);
|
|
25
|
+
if (part.kind === "tally") {
|
|
26
|
+
if (!tallyVar) throw new SerializeError("tally outside a counted check");
|
|
27
|
+
return js`String(${tallyVar})`;
|
|
28
|
+
}
|
|
29
|
+
return js`String(${expr(ctx, part)})`;
|
|
30
|
+
});
|
|
31
|
+
if (parts.length === 0) return str("");
|
|
32
|
+
return parts.length === 1 ? parts[0]! : js`(${join(" + ", parts)})`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders a LowerParams map to an object-literal expression, mirroring
|
|
37
|
+
* renderError's includeParams shape. `tallyVar` binds tally placeholders
|
|
38
|
+
* exactly as in {@link renderMessage}.
|
|
39
|
+
*/
|
|
40
|
+
export function paramsChunk(
|
|
41
|
+
ctx: UnitContext,
|
|
42
|
+
params: LowerParams | undefined,
|
|
43
|
+
tallyVar?: CodeChunk,
|
|
44
|
+
tallyListVar?: CodeChunk,
|
|
45
|
+
): CodeChunk {
|
|
46
|
+
// pushError drops the chunk entirely when params are off — don't render
|
|
47
|
+
// (a tallyList reference has no accumulator to bind to in that mode).
|
|
48
|
+
if (!ctx.listParams || params === undefined) return js`{}`;
|
|
49
|
+
const entries = Object.entries(params).map(([key, part]) => {
|
|
50
|
+
let value: CodeChunk;
|
|
51
|
+
if (part.kind === "tally" || part.kind === "tallyList") {
|
|
52
|
+
const bound = part.kind === "tally" ? tallyVar : tallyListVar;
|
|
53
|
+
if (!bound)
|
|
54
|
+
throw new SerializeError(part.kind + " outside a counted check");
|
|
55
|
+
value = bound;
|
|
56
|
+
} else {
|
|
57
|
+
value = expr(ctx, part);
|
|
58
|
+
}
|
|
59
|
+
return js`${str(key)}: ${value}`;
|
|
60
|
+
});
|
|
61
|
+
return js`{ ${join(", ", entries)} }`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* List-mode failure: mark the unit invalid and push an interpreter-exact
|
|
66
|
+
* error unit (renderError's shape — keyword suffix escaped on both
|
|
67
|
+
* paths). Unit objects materialize only here, on the failure path (D9e).
|
|
68
|
+
*/
|
|
69
|
+
export function pushError(
|
|
70
|
+
ctx: UnitContext,
|
|
71
|
+
msg: CodeChunk,
|
|
72
|
+
withKeyword = true,
|
|
73
|
+
params?: CodeChunk,
|
|
74
|
+
): CodeChunk {
|
|
75
|
+
const suffix = withKeyword ? "/" + escapeSegment(ctx.currentKeyword) : "";
|
|
76
|
+
const sloc = ctx.unit.ref.baseUri + "#" + ctx.unit.ref.pointer + suffix;
|
|
77
|
+
const vocab =
|
|
78
|
+
ctx.currentVocab !== null
|
|
79
|
+
? js`vocabulary: ${str(ctx.currentVocab)}, `
|
|
80
|
+
: js``;
|
|
81
|
+
const extra = ctx.listParams
|
|
82
|
+
? withKeyword
|
|
83
|
+
? js`, keyword: ${str(ctx.currentKeyword)}, ${vocab}params: ${params ?? js`{}`}`
|
|
84
|
+
: js`, params: ${params ?? js`{}`}`
|
|
85
|
+
: js``;
|
|
86
|
+
const fail = ctx.kwOk
|
|
87
|
+
? js`ok = false; ${ctx.kwOk} = false;`
|
|
88
|
+
: js`ok = false;`;
|
|
89
|
+
const unit = js`{ evaluationPath: ${id("ep")} + ${str(suffix)}, schemaLocation: ${str(sloc)}, inputLocation: ${id("ip")}, error: ${msg}${extra} }`;
|
|
90
|
+
// Trace emission records the raising application with the unit.
|
|
91
|
+
return ctx.trace
|
|
92
|
+
? js`${fail} ${id("h_err")}(${ST}, ${TN}, ${unit});`
|
|
93
|
+
: js`${fail} ${ERRS}.push(${unit});`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The error channel's current length: the value a relevance mark takes. */
|
|
97
|
+
export function errsLength(ctx: UnitContext): CodeChunk {
|
|
98
|
+
return ctx.trace ? js`${ST}.errs.length` : js`${ERRS}.length`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Drop the errors pushed since mark `m` (an accepting keyword made them
|
|
103
|
+
* irrelevant). Trace emission routes the cut through the runtime, which
|
|
104
|
+
* discards or retains them by the artifact's level.
|
|
105
|
+
*/
|
|
106
|
+
export function errsCut(ctx: UnitContext, m: CodeChunk): CodeChunk {
|
|
107
|
+
return ctx.trace
|
|
108
|
+
? js`${id("h_cutE")}(${ST}, ${m});`
|
|
109
|
+
: js`${ERRS}.length = ${m};`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Error relevance (draft-03 §12.2) in list mode: a keyword that accepts
|
|
114
|
+
* makes the errors its sub-evaluations pushed irrelevant. Callers take the
|
|
115
|
+
* mark before the keyword's applies and truncate on the accept path;
|
|
116
|
+
* every branch's errors land in the shared `errs` between the two.
|
|
117
|
+
*/
|
|
118
|
+
export function errMark(ctx: UnitContext): CodeChunk | null {
|
|
119
|
+
return ctx.output === "list" ? id("m" + String(ctx.counters.temp++)) : null;
|
|
120
|
+
}
|