@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/emit.d.ts +52 -0
  4. package/dist/emit.d.ts.map +1 -0
  5. package/dist/emit.js +166 -0
  6. package/dist/emit.js.map +1 -0
  7. package/dist/explain.d.ts +28 -0
  8. package/dist/explain.d.ts.map +1 -0
  9. package/dist/explain.js +42 -0
  10. package/dist/explain.js.map +1 -0
  11. package/dist/index.d.ts +132 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +169 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/plan.d.ts +83 -0
  16. package/dist/plan.d.ts.map +1 -0
  17. package/dist/plan.js +385 -0
  18. package/dist/plan.js.map +1 -0
  19. package/dist/runtime-compile.d.ts +30 -0
  20. package/dist/runtime-compile.d.ts.map +1 -0
  21. package/dist/runtime-compile.js +29 -0
  22. package/dist/runtime-compile.js.map +1 -0
  23. package/dist/runtime.d.ts +157 -0
  24. package/dist/runtime.d.ts.map +1 -0
  25. package/dist/runtime.js +336 -0
  26. package/dist/runtime.js.map +1 -0
  27. package/dist/serialize/apply.d.ts +28 -0
  28. package/dist/serialize/apply.d.ts.map +1 -0
  29. package/dist/serialize/apply.js +211 -0
  30. package/dist/serialize/apply.js.map +1 -0
  31. package/dist/serialize/context.d.ts +143 -0
  32. package/dist/serialize/context.d.ts.map +1 -0
  33. package/dist/serialize/context.js +93 -0
  34. package/dist/serialize/context.js.map +1 -0
  35. package/dist/serialize/expressions.d.ts +7 -0
  36. package/dist/serialize/expressions.d.ts.map +1 -0
  37. package/dist/serialize/expressions.js +177 -0
  38. package/dist/serialize/expressions.js.map +1 -0
  39. package/dist/serialize/guards.d.ts +7 -0
  40. package/dist/serialize/guards.d.ts.map +1 -0
  41. package/dist/serialize/guards.js +19 -0
  42. package/dist/serialize/guards.js.map +1 -0
  43. package/dist/serialize/index.d.ts +7 -0
  44. package/dist/serialize/index.d.ts.map +1 -0
  45. package/dist/serialize/index.js +266 -0
  46. package/dist/serialize/index.js.map +1 -0
  47. package/dist/serialize/keywords.d.ts +32 -0
  48. package/dist/serialize/keywords.d.ts.map +1 -0
  49. package/dist/serialize/keywords.js +131 -0
  50. package/dist/serialize/keywords.js.map +1 -0
  51. package/dist/serialize/messages.d.ts +36 -0
  52. package/dist/serialize/messages.d.ts.map +1 -0
  53. package/dist/serialize/messages.js +100 -0
  54. package/dist/serialize/messages.js.map +1 -0
  55. package/dist/serialize/names.d.ts +47 -0
  56. package/dist/serialize/names.d.ts.map +1 -0
  57. package/dist/serialize/names.js +115 -0
  58. package/dist/serialize/names.js.map +1 -0
  59. package/dist/serialize/spans.d.ts +34 -0
  60. package/dist/serialize/spans.d.ts.map +1 -0
  61. package/dist/serialize/spans.js +61 -0
  62. package/dist/serialize/spans.js.map +1 -0
  63. package/dist/serialize/statements.d.ts +16 -0
  64. package/dist/serialize/statements.d.ts.map +1 -0
  65. package/dist/serialize/statements.js +396 -0
  66. package/dist/serialize/statements.js.map +1 -0
  67. package/dist/serialize/unit.d.ts +8 -0
  68. package/dist/serialize/unit.d.ts.map +1 -0
  69. package/dist/serialize/unit.js +107 -0
  70. package/dist/serialize/unit.js.map +1 -0
  71. package/dist/standalone.d.ts +24 -0
  72. package/dist/standalone.d.ts.map +1 -0
  73. package/dist/standalone.js +166 -0
  74. package/dist/standalone.js.map +1 -0
  75. package/package.json +38 -0
  76. package/src/emit.ts +196 -0
  77. package/src/explain.ts +67 -0
  78. package/src/index.ts +366 -0
  79. package/src/plan.ts +496 -0
  80. package/src/runtime-compile.ts +90 -0
  81. package/src/runtime.ts +599 -0
  82. package/src/serialize/apply.ts +251 -0
  83. package/src/serialize/context.ts +161 -0
  84. package/src/serialize/expressions.ts +202 -0
  85. package/src/serialize/guards.ts +20 -0
  86. package/src/serialize/index.ts +432 -0
  87. package/src/serialize/keywords.ts +144 -0
  88. package/src/serialize/messages.ts +120 -0
  89. package/src/serialize/names.ts +136 -0
  90. package/src/serialize/spans.ts +88 -0
  91. package/src/serialize/statements.ts +531 -0
  92. package/src/serialize/unit.ts +118 -0
  93. 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
+ }