@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,251 @@
|
|
|
1
|
+
// Subschema applications: the unit/trampoline call for a LowerApply, its
|
|
2
|
+
// evaluation-path and instance-pointer arguments, and D9c inlining.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
escapeSegment,
|
|
6
|
+
type LowerApply,
|
|
7
|
+
type LowerCursor,
|
|
8
|
+
} from "@json-schema-engine/core";
|
|
9
|
+
import { type CodeChunk, id, join, js, num, str } from "../emit.js";
|
|
10
|
+
import {
|
|
11
|
+
D,
|
|
12
|
+
S,
|
|
13
|
+
T,
|
|
14
|
+
TN,
|
|
15
|
+
unitFn,
|
|
16
|
+
unitFnRegion,
|
|
17
|
+
bindingVar,
|
|
18
|
+
channelArgs,
|
|
19
|
+
fragHelper,
|
|
20
|
+
shapeOf,
|
|
21
|
+
} from "./names.js";
|
|
22
|
+
import { UnitContext, SerializeError } from "./context.js";
|
|
23
|
+
import { guardDecl } from "./guards.js";
|
|
24
|
+
import { expr } from "./expressions.js";
|
|
25
|
+
import { unitBody } from "./unit.js";
|
|
26
|
+
|
|
27
|
+
export function applyCall(ctx: UnitContext, apply: LowerApply): CodeChunk {
|
|
28
|
+
// Resolve the target exactly as the planner did.
|
|
29
|
+
let targetKey: string;
|
|
30
|
+
if (apply.ref !== undefined) {
|
|
31
|
+
targetKey = edgeTarget(ctx, apply.ref, null);
|
|
32
|
+
} else {
|
|
33
|
+
targetKey = edgeTarget(ctx, null, apply);
|
|
34
|
+
}
|
|
35
|
+
const valueExpr = cursorValue(ctx, apply.cursor);
|
|
36
|
+
const target = ctx.plan.units.get(targetKey);
|
|
37
|
+
if (!target) {
|
|
38
|
+
throw new SerializeError("apply target '" + targetKey + "' not planned");
|
|
39
|
+
}
|
|
40
|
+
const locate = (): [CodeChunk, CodeChunk] => [
|
|
41
|
+
applyEp(ctx, apply),
|
|
42
|
+
applyIp(ctx, apply),
|
|
43
|
+
];
|
|
44
|
+
if (target.kind === "static") {
|
|
45
|
+
// Boolean subschemas fold to literals — except in list mode, where a
|
|
46
|
+
// `false` schema must report its "schema is false" error unit, and in
|
|
47
|
+
// trace emission, where every application is a node.
|
|
48
|
+
if (typeof target.ref.node === "boolean") {
|
|
49
|
+
if (ctx.output !== "list") {
|
|
50
|
+
return target.ref.node ? js`true` : js`false`;
|
|
51
|
+
}
|
|
52
|
+
if (target.ref.node && !ctx.trace) return js`true`;
|
|
53
|
+
}
|
|
54
|
+
const scope = ctx.unit.reachesInterpreted ? S : id("h_s0");
|
|
55
|
+
ctx.calledUnit = true;
|
|
56
|
+
// Region emission: an in-place apply threads the channel through the
|
|
57
|
+
// target's region variant (rule 5). Such a target is always a region
|
|
58
|
+
// member — a would-be tracked one was islanded, a boolean folded above
|
|
59
|
+
// or (list-mode `false`) reports without producing, so it keeps the
|
|
60
|
+
// plain variant. Child-cursor applies use the plain variant too (a
|
|
61
|
+
// child location's coverage never joins this unit's channel).
|
|
62
|
+
const inPlaceRegion =
|
|
63
|
+
ctx.regionMode &&
|
|
64
|
+
apply.cursor.kind === "here" &&
|
|
65
|
+
typeof target.ref.node !== "boolean";
|
|
66
|
+
if (inPlaceRegion && !target.inRegion) {
|
|
67
|
+
throw new SerializeError(
|
|
68
|
+
"in-place region target '" + targetKey + "' is not a region member",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const fn = inPlaceRegion
|
|
72
|
+
? unitFnRegion(ctx.fnIndex.get(targetKey)!)
|
|
73
|
+
: unitFn(ctx.fnIndex.get(targetKey)!);
|
|
74
|
+
const args = channelArgs(shapeOf(ctx), locate, TN, inPlaceRegion);
|
|
75
|
+
return js`${fn}(${join(", ", [valueExpr, D, scope, ...args])})`;
|
|
76
|
+
}
|
|
77
|
+
const slot = num(ctx.tableIndex.get(targetKey)!);
|
|
78
|
+
ctx.calledUnit = true;
|
|
79
|
+
// Region emission: an in-place island harvests its root coverage into the
|
|
80
|
+
// channel (rule 7) — fragCov in flag mode, the trailing-`ev` overloads of
|
|
81
|
+
// the list trampolines otherwise. A child-cursor island stays plain.
|
|
82
|
+
const inPlaceRegion = ctx.regionMode && apply.cursor.kind === "here";
|
|
83
|
+
const helper = fragHelper(shapeOf(ctx), inPlaceRegion);
|
|
84
|
+
const args = channelArgs(shapeOf(ctx), locate, TN, inPlaceRegion);
|
|
85
|
+
return js`${helper}(${join(", ", [js`${T}[${slot}]`, valueExpr, S, D, ...args])})`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The child's evaluation-path prefix: the caller's `ep` plus this apply's
|
|
90
|
+
* constant segment chain — the keyword name (or the driven sibling's, or
|
|
91
|
+
* just the reference keyword for ref applies) plus any static path
|
|
92
|
+
* segments. Bindings never appear in apply paths (only in cursors), so
|
|
93
|
+
* the suffix is always a compile-time constant.
|
|
94
|
+
*/
|
|
95
|
+
export function applyEp(ctx: UnitContext, apply: LowerApply): CodeChunk {
|
|
96
|
+
const segments =
|
|
97
|
+
apply.sibling !== undefined
|
|
98
|
+
? [apply.sibling, ...apply.path]
|
|
99
|
+
: [ctx.currentKeyword, ...apply.path];
|
|
100
|
+
const suffix = segments
|
|
101
|
+
.map((seg) => {
|
|
102
|
+
if (typeof seg === "object") {
|
|
103
|
+
throw new SerializeError("binding segments are cursor-only");
|
|
104
|
+
}
|
|
105
|
+
return "/" + escapeSegment(String(seg));
|
|
106
|
+
})
|
|
107
|
+
.join("");
|
|
108
|
+
return js`${id("ep")} + ${str(suffix)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The child's instance-pointer prefix (RFC 6901-escaped at runtime for swept names). */
|
|
112
|
+
export function applyIp(ctx: UnitContext, apply: LowerApply): CodeChunk {
|
|
113
|
+
const cursor = apply.cursor;
|
|
114
|
+
if (cursor.kind === "here") return id("ip");
|
|
115
|
+
if (cursor.kind === "key") {
|
|
116
|
+
// propertyNames: the violation's instance location points at the
|
|
117
|
+
// property itself (interpreter: childCursor(parent, name, name)).
|
|
118
|
+
return js`${id("ip")} + "/" + ${id("h_esc")}(String(${bindingVar(cursor.binding)}))`;
|
|
119
|
+
}
|
|
120
|
+
if (cursor.of.kind !== "here") {
|
|
121
|
+
throw new SerializeError("nested child cursors are not emitted (M6)");
|
|
122
|
+
}
|
|
123
|
+
const seg = cursor.segment;
|
|
124
|
+
if (typeof seg === "string") {
|
|
125
|
+
return js`${id("ip")} + ${str("/" + escapeSegment(seg))}`;
|
|
126
|
+
}
|
|
127
|
+
if (typeof seg === "number") {
|
|
128
|
+
return js`${id("ip")} + ${str("/" + String(seg))}`;
|
|
129
|
+
}
|
|
130
|
+
return js`${id("ip")} + "/" + ${id("h_esc")}(String(${expr(ctx, seg)}))`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The planner recorded edges in keyword order; match an apply back to its
|
|
134
|
+
// edge by keyword + sibling + path/ref identity. `sibling` disambiguates a
|
|
135
|
+
// keyword that emits more than one apply at the same path (if's condition
|
|
136
|
+
// vs. its then/else edges, all path: []).
|
|
137
|
+
export function edgeTarget(
|
|
138
|
+
ctx: UnitContext,
|
|
139
|
+
ref: string | null,
|
|
140
|
+
apply: LowerApply | null,
|
|
141
|
+
): string {
|
|
142
|
+
for (const edge of ctx.unit.edges) {
|
|
143
|
+
if (edge.keyword !== ctx.currentKeyword) continue;
|
|
144
|
+
if ((edge.app.sibling ?? null) !== (apply?.sibling ?? null)) continue;
|
|
145
|
+
if (ref !== null) {
|
|
146
|
+
if (edge.app.ref === ref) return edge.targetKey;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const want = apply!.path.map((p) =>
|
|
150
|
+
typeof p === "object" ? "*" : String(p),
|
|
151
|
+
);
|
|
152
|
+
const got = edge.app.path.map(String);
|
|
153
|
+
// Sweep applications bind runtime segments; their planned path is the
|
|
154
|
+
// static prefix (often empty).
|
|
155
|
+
if (want.filter((w) => w !== "*").join("\u0000") === got.join("\u0000"))
|
|
156
|
+
return edge.targetKey;
|
|
157
|
+
}
|
|
158
|
+
throw new SerializeError(
|
|
159
|
+
"no planned edge for keyword '" + ctx.currentKeyword + "'",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function cursorValue(ctx: UnitContext, cursor: LowerCursor): CodeChunk {
|
|
164
|
+
if (cursor.kind === "here") return ctx.valueVar;
|
|
165
|
+
// propertyNames: the loop binding IS the instance (the key string),
|
|
166
|
+
// not a child reached by descending from a parent cursor.
|
|
167
|
+
if (cursor.kind === "key") return bindingVar(cursor.binding);
|
|
168
|
+
const base = cursorValue(ctx, cursor.of);
|
|
169
|
+
const seg = cursor.segment;
|
|
170
|
+
if (typeof seg === "string") return js`${base}[${str(seg)}]`;
|
|
171
|
+
if (typeof seg === "number") return js`${base}[${num(seg)}]`;
|
|
172
|
+
return js`${base}[${expr(ctx, seg)}]`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* D9c inlining: a single-use, static, non-island, non-boolean target of an
|
|
177
|
+
* allMustPass apply expands into the caller — its lowered `return false`
|
|
178
|
+
* IS the caller's correct failure action, so the body drops in verbatim
|
|
179
|
+
* with the instance rebound to a temp. The inline stack guards
|
|
180
|
+
* self-recursion; recursive chains keep their function calls (the depth
|
|
181
|
+
* budget bounds them). Inlined units skip the per-call depth tick: their
|
|
182
|
+
* nesting is statically bounded, and maxDepth is a resource bound, not an
|
|
183
|
+
* exact-count contract (D20/§7).
|
|
184
|
+
*/
|
|
185
|
+
export function tryInline(
|
|
186
|
+
ctx: UnitContext,
|
|
187
|
+
apply: LowerApply,
|
|
188
|
+
): CodeChunk | null {
|
|
189
|
+
if (!ctx.flags.inline) return null;
|
|
190
|
+
// Region emission never inlines: an in-place target is called through its
|
|
191
|
+
// channel-threaded region variant, not expanded (rule 8). Trace emission
|
|
192
|
+
// never inlines either: an application is a node only as a call.
|
|
193
|
+
if (ctx.regionMode || ctx.trace) return null;
|
|
194
|
+
let targetKey: string;
|
|
195
|
+
if (apply.ref !== undefined) {
|
|
196
|
+
targetKey = edgeTarget(ctx, apply.ref, null);
|
|
197
|
+
} else {
|
|
198
|
+
targetKey = edgeTarget(ctx, null, apply);
|
|
199
|
+
}
|
|
200
|
+
const target = ctx.plan.units.get(targetKey);
|
|
201
|
+
if (
|
|
202
|
+
target?.kind !== "static" ||
|
|
203
|
+
typeof target.ref.node === "boolean" ||
|
|
204
|
+
target.useCount !== 1 ||
|
|
205
|
+
target.reachesInterpreted ||
|
|
206
|
+
// A tracked unit owns a local channel and a region member is entered
|
|
207
|
+
// through its channel-threaded variant; neither can be expanded verbatim
|
|
208
|
+
// into a plain caller (phase B).
|
|
209
|
+
target.tracking ||
|
|
210
|
+
target.inRegion ||
|
|
211
|
+
ctx.inlineStack.has(targetKey) ||
|
|
212
|
+
ctx.inlineStack.size > 32
|
|
213
|
+
) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
// A `here` cursor applies at the same instance value: reuse the host's
|
|
217
|
+
// variable and its object guard instead of aliasing through a temp.
|
|
218
|
+
const here = apply.cursor.kind === "here";
|
|
219
|
+
const childVar = here ? ctx.valueVar : id("t" + String(ctx.counters.temp++));
|
|
220
|
+
const child = new UnitContext(
|
|
221
|
+
target,
|
|
222
|
+
ctx.plan,
|
|
223
|
+
ctx.registry,
|
|
224
|
+
ctx.fnIndex,
|
|
225
|
+
ctx.tableIndex,
|
|
226
|
+
childVar,
|
|
227
|
+
ctx.counters,
|
|
228
|
+
new Set([...ctx.inlineStack, targetKey]),
|
|
229
|
+
here ? ctx : null,
|
|
230
|
+
ctx.flags,
|
|
231
|
+
ctx.output,
|
|
232
|
+
ctx.listParams,
|
|
233
|
+
ctx.annMode,
|
|
234
|
+
ctx.annKeep,
|
|
235
|
+
false,
|
|
236
|
+
new Set(),
|
|
237
|
+
ctx.trace,
|
|
238
|
+
);
|
|
239
|
+
const body = unitBody(child);
|
|
240
|
+
if (child.calledUnit) ctx.calledUnit = true;
|
|
241
|
+
ctx.inlinedKeys.add(targetKey);
|
|
242
|
+
for (const k of child.inlinedKeys) ctx.inlinedKeys.add(k);
|
|
243
|
+
const parts: CodeChunk[] = [];
|
|
244
|
+
if (!here) {
|
|
245
|
+
parts.push(js`const ${childVar} = ${cursorValue(ctx, apply.cursor)};`);
|
|
246
|
+
}
|
|
247
|
+
const childGuard = guardDecl(child);
|
|
248
|
+
if (childGuard) parts.push(childGuard);
|
|
249
|
+
parts.push(...body);
|
|
250
|
+
return join("\n", parts);
|
|
251
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// The per-unit emission state every serializer module reads and writes:
|
|
2
|
+
// plan lookups, the keyword being emitted, the channel accumulators, and
|
|
3
|
+
// the D9 knobs. Behavior lives in the sibling modules as functions over it.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type RecordPredicate,
|
|
7
|
+
type AnnotationSelection,
|
|
8
|
+
type SchemaRegistry,
|
|
9
|
+
} from "@json-schema-engine/core";
|
|
10
|
+
import { type CodeChunk } from "../emit.js";
|
|
11
|
+
import type { CompilationPlan, PlannedUnit } from "../plan.js";
|
|
12
|
+
|
|
13
|
+
/** Thrown for an IR shape the serializer cannot emit (a planner/lowering bug). */
|
|
14
|
+
export class SerializeError extends Error {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Annotation-mode options threaded through serialization. Its presence (with
|
|
18
|
+
* `output === "list"`) turns on annotation collection; the selection's
|
|
19
|
+
* allow/deny lists are applied statically at annotate sites (the compiled
|
|
20
|
+
* analogue of annotation elision — the same {@link makeRecordPredicate}
|
|
21
|
+
* decision the interpreter's renderer makes). The `keep` predicate runs at
|
|
22
|
+
* runtime in the artifact wrapper, never here.
|
|
23
|
+
*/
|
|
24
|
+
export interface AnnotateOptions {
|
|
25
|
+
selection?: boolean | AnnotationSelection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emission mode: "runtime" artifacts close over the Runtime object R
|
|
30
|
+
* (instantiated via new Function); "standalone" artifacts are self-contained
|
|
31
|
+
* ES modules whose preamble (standalone.ts) defines the same h_-named
|
|
32
|
+
* helpers, so the body serialization is identical.
|
|
33
|
+
*/
|
|
34
|
+
export type EmitMode = "runtime" | "standalone";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optimization switches (M6.5). `conservative` artifacts disable inlining
|
|
38
|
+
* and the plain-data fast paths — the fuzzer runs both configurations so an
|
|
39
|
+
* optimization can never change a verdict unnoticed.
|
|
40
|
+
*/
|
|
41
|
+
export interface EmitFlags {
|
|
42
|
+
inline: boolean;
|
|
43
|
+
plainData: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Output tier of the artifact (D10/M7-adjacent): "flag" = verdict-only,
|
|
48
|
+
* fail-fast, zero allocation on the hot path; "list" = full error
|
|
49
|
+
* collection with interpreter-exact units — no fail-fast, no
|
|
50
|
+
* short-circuit, every branch runs (DESIGN §7 licensing), and error-unit
|
|
51
|
+
* objects materialize only on failure paths (D9e).
|
|
52
|
+
*/
|
|
53
|
+
export type EmitOutput = "flag" | "list";
|
|
54
|
+
export const DEFAULT_FLAGS: EmitFlags = { inline: true, plainData: true };
|
|
55
|
+
|
|
56
|
+
/** How a plan is serialized; every field has the flag-mode default. */
|
|
57
|
+
export interface SerializeOptions {
|
|
58
|
+
mode?: EmitMode;
|
|
59
|
+
flags?: EmitFlags;
|
|
60
|
+
output?: EmitOutput;
|
|
61
|
+
/** structured `keyword`/`vocabulary`/`params` on error units (D13) */
|
|
62
|
+
listParams?: boolean;
|
|
63
|
+
/** annotation collection (a list-mode variant) */
|
|
64
|
+
annotate?: AnnotateOptions;
|
|
65
|
+
/**
|
|
66
|
+
* Trace emission (a list-mode variant): every application records its
|
|
67
|
+
* node in a located tree — locations, keyword verdicts, and the records
|
|
68
|
+
* it raised — so the artifact renders every output format.
|
|
69
|
+
*/
|
|
70
|
+
trace?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Per-unit serialization state: bindings, keyword context, apply targets. */
|
|
74
|
+
export interface Counters {
|
|
75
|
+
binding: number;
|
|
76
|
+
tally: number;
|
|
77
|
+
temp: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The accumulator(s) a keyword's produce recipe reads: names build a deduped
|
|
82
|
+
* `Set`, indexes a max tracker / applied flag / matched list. Set by
|
|
83
|
+
* `beginKeyword` (keywords.ts) for a consumed producer in region emission,
|
|
84
|
+
* and driven by the application call sites within the keyword's own
|
|
85
|
+
* statements.
|
|
86
|
+
*/
|
|
87
|
+
export interface KeywordAnn {
|
|
88
|
+
produceKind:
|
|
89
|
+
"collectedNames" | "largestOrTrue" | "appliedTrue" | "matchedOrAllTrue";
|
|
90
|
+
names?: CodeChunk;
|
|
91
|
+
max?: CodeChunk;
|
|
92
|
+
applied?: CodeChunk;
|
|
93
|
+
matched?: CodeChunk;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class UnitContext {
|
|
97
|
+
currentKeyword = "";
|
|
98
|
+
currentVocab: string | null = null;
|
|
99
|
+
// State for the keyword currently being emitted: whether its annotate is
|
|
100
|
+
// retained (static lists), whether its produce feeds the runtime coverage
|
|
101
|
+
// channel (consumed producer), and the accumulators the produce reads. The
|
|
102
|
+
// two flags are independent — retention must never affect coverage.
|
|
103
|
+
annKwKept = false;
|
|
104
|
+
covKwKept = false;
|
|
105
|
+
annKw: KeywordAnn | null = null;
|
|
106
|
+
// List-mode channel producers gate their push on the keyword's OWN verdict
|
|
107
|
+
// (Appendix D: dependency data only from an accepting keyword); the unit's
|
|
108
|
+
// `ok` cannot serve, since list mode continues past a failed sibling.
|
|
109
|
+
kwOk: CodeChunk | null = null;
|
|
110
|
+
// Object-guard CSE: one `const gN = (typeof x === "object" && …)` per
|
|
111
|
+
// unit value, prepended by the body builder when used. Inlined `here`-
|
|
112
|
+
// cursor children share the parent's guard (same value, same variable).
|
|
113
|
+
objGuardVar: CodeChunk | null = null;
|
|
114
|
+
objGuardUsed = false;
|
|
115
|
+
/** set when this unit's body (incl. inlines) emits any unit/frag call */
|
|
116
|
+
calledUnit = false;
|
|
117
|
+
// Region emission (phase B): the current keyword's behavior id (for the
|
|
118
|
+
// consumed-producer channel-push gate) and the JS variable + half bound by
|
|
119
|
+
// each coverageFold, read by a following coverageCovers.
|
|
120
|
+
currentBehaviorId = "";
|
|
121
|
+
coverageFolds = new Map<
|
|
122
|
+
number,
|
|
123
|
+
{ readonly var: CodeChunk; readonly half: "names" | "indexes" }
|
|
124
|
+
>();
|
|
125
|
+
|
|
126
|
+
/** Unit keys this context (transitively) inlined — their functions are omitted. */
|
|
127
|
+
readonly inlinedKeys = new Set<string>();
|
|
128
|
+
/**
|
|
129
|
+
* Trace emission: the verdict variable of each present keyword,
|
|
130
|
+
* pre-declared for the unit because `if` settles `then`/`else`'s verdicts
|
|
131
|
+
* before those keywords' own (empty) statements run.
|
|
132
|
+
*/
|
|
133
|
+
readonly kwVerdicts = new Map<string, CodeChunk>();
|
|
134
|
+
|
|
135
|
+
constructor(
|
|
136
|
+
readonly unit: PlannedUnit,
|
|
137
|
+
readonly plan: CompilationPlan,
|
|
138
|
+
readonly registry: SchemaRegistry,
|
|
139
|
+
readonly fnIndex: Map<string, number>,
|
|
140
|
+
readonly tableIndex: Map<string, number>,
|
|
141
|
+
/** JS variable holding this unit's instance value (V, or an inline temp) */
|
|
142
|
+
readonly valueVar: CodeChunk,
|
|
143
|
+
/** shared per-function counters so inlined bodies never collide */
|
|
144
|
+
readonly counters: Counters,
|
|
145
|
+
/** unit keys on the current inline chain (self-inline guard) */
|
|
146
|
+
readonly inlineStack: ReadonlySet<string>,
|
|
147
|
+
/** parent context sharing the same instance value (here-cursor inline) */
|
|
148
|
+
readonly guardParent: UnitContext | null = null,
|
|
149
|
+
readonly flags: EmitFlags = DEFAULT_FLAGS,
|
|
150
|
+
readonly output: EmitOutput = "flag",
|
|
151
|
+
readonly listParams = false,
|
|
152
|
+
readonly annMode = false,
|
|
153
|
+
readonly annKeep: RecordPredicate | null = null,
|
|
154
|
+
/** region emission: thread the runtime coverage channel `ev` (phase B) */
|
|
155
|
+
readonly regionMode = false,
|
|
156
|
+
/** behavior ids a consumer observes (region channel-push gate, rule 5) */
|
|
157
|
+
readonly coverageIds: ReadonlySet<string> = new Set(),
|
|
158
|
+
/** trace emission: record the application tree (SerializeOptions.trace) */
|
|
159
|
+
readonly trace = false,
|
|
160
|
+
) {}
|
|
161
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// LowerExpr rendering: instance access, type tests, helper calls, and the
|
|
2
|
+
// coverage-fold membership tests of region emission.
|
|
3
|
+
|
|
4
|
+
import { type LowerExpr } from "@json-schema-engine/core";
|
|
5
|
+
import { type CodeChunk, id, join, js, raw, str, json } from "../emit.js";
|
|
6
|
+
import { bindingVar, regexConst, formatConst } from "./names.js";
|
|
7
|
+
import { UnitContext, SerializeError } from "./context.js";
|
|
8
|
+
import { ensureObjGuard } from "./guards.js";
|
|
9
|
+
import { applyCall } from "./apply.js";
|
|
10
|
+
|
|
11
|
+
export function expr(ctx: UnitContext, e: LowerExpr): CodeChunk {
|
|
12
|
+
switch (e.kind) {
|
|
13
|
+
case "instance":
|
|
14
|
+
return ctx.valueVar;
|
|
15
|
+
case "const":
|
|
16
|
+
return json(e.value);
|
|
17
|
+
case "member":
|
|
18
|
+
return js`${expr(ctx, e.target)}[${str(e.key)}]`;
|
|
19
|
+
case "item":
|
|
20
|
+
return js`${expr(ctx, e.target)}[${expr(ctx, e.index)}]`;
|
|
21
|
+
case "binding":
|
|
22
|
+
return bindingVar(e.id);
|
|
23
|
+
case "typeIs":
|
|
24
|
+
return typeTest(ctx, e.target, e.types);
|
|
25
|
+
case "hasOwn": {
|
|
26
|
+
// Plain-data instance contract (DESIGN §7, M6.5): for JSON data,
|
|
27
|
+
// presence-of-own-key is `key in obj` — plain data has no inherited
|
|
28
|
+
// enumerables, so the prototype chain `in` walks holds nothing but
|
|
29
|
+
// Object.prototype's own names. Keys that live there (constructor,
|
|
30
|
+
// toString, …) or are "__proto__" would false-positive through that
|
|
31
|
+
// chain, so those keep an explicit own-check.
|
|
32
|
+
//
|
|
33
|
+
// `in` with a constant key is the only probe fast on both record
|
|
34
|
+
// shapes (E12, bench/corpora records-*, 2000 records): on uniform
|
|
35
|
+
// records it ties the `obj[key] !== undefined` load it replaces
|
|
36
|
+
// (0.033 ms), while on sparse records — where a value load goes
|
|
37
|
+
// megamorphic over differing shapes — it costs 4.1 ms against that
|
|
38
|
+
// load's 22.7 ms. The own-check forms measure the other way round:
|
|
39
|
+
// ~2.8 ms sparse but ~1.7 ms uniform, a ~50x uniform regression.
|
|
40
|
+
if (typeof e.key === "string") {
|
|
41
|
+
const dangerous = e.key === "__proto__" || e.key in Object.prototype;
|
|
42
|
+
if (ctx.flags.plainData && !dangerous) {
|
|
43
|
+
return js`(${str(e.key)} in ${expr(ctx, e.target)})`;
|
|
44
|
+
}
|
|
45
|
+
return js`${id("h_hop")}.call(${expr(ctx, e.target)}, ${str(e.key)})`;
|
|
46
|
+
}
|
|
47
|
+
return js`${id("h_hop")}.call(${expr(ctx, e.target)}, ${expr(ctx, e.key)})`;
|
|
48
|
+
}
|
|
49
|
+
case "cmp":
|
|
50
|
+
return js`(${expr(ctx, e.left)} ${raw(e.op)} ${expr(ctx, e.right)})`;
|
|
51
|
+
case "helper":
|
|
52
|
+
return helperCall(ctx, e.helper, e.args);
|
|
53
|
+
case "regexTest": {
|
|
54
|
+
let idx = ctx.plan.patterns.indexOf(e.source);
|
|
55
|
+
if (idx === -1) {
|
|
56
|
+
// Coverage patterns can first appear here; the prologue is built
|
|
57
|
+
// after all units serialize, so late additions still hoist.
|
|
58
|
+
idx = ctx.plan.patterns.push(e.source) - 1;
|
|
59
|
+
}
|
|
60
|
+
return js`${regexConst(idx)}.test(${expr(ctx, e.target)})`;
|
|
61
|
+
}
|
|
62
|
+
case "formatTest": {
|
|
63
|
+
let idx = ctx.plan.formats.indexOf(e.name);
|
|
64
|
+
if (idx === -1) {
|
|
65
|
+
// The prologue hoists after all units serialize (as with regexTest),
|
|
66
|
+
// so a name first seen here still gets its lookup const.
|
|
67
|
+
idx = ctx.plan.formats.push(e.name) - 1;
|
|
68
|
+
}
|
|
69
|
+
return js`${formatConst(idx)}.test(${expr(ctx, e.target)})`;
|
|
70
|
+
}
|
|
71
|
+
case "not":
|
|
72
|
+
return js`!(${expr(ctx, e.expr)})`;
|
|
73
|
+
case "logic": {
|
|
74
|
+
const op = e.op === "and" ? " && " : " || ";
|
|
75
|
+
return js`(${join(
|
|
76
|
+
op,
|
|
77
|
+
e.parts.map((p) => expr(ctx, p)),
|
|
78
|
+
)})`;
|
|
79
|
+
}
|
|
80
|
+
case "tally":
|
|
81
|
+
case "tallyList":
|
|
82
|
+
throw new SerializeError(
|
|
83
|
+
"'" + e.kind + "' is only meaningful inside a combineCheck message",
|
|
84
|
+
);
|
|
85
|
+
case "applyExpr":
|
|
86
|
+
// The two legal applyExpr positions (`if`'s condition, contains'
|
|
87
|
+
// probe) are intercepted at the statement level so their span can
|
|
88
|
+
// mark/truncate. Reaching here in annotation mode means an
|
|
89
|
+
// unrecognized shape whose records could not be discarded — fail
|
|
90
|
+
// loud rather than lose annotations silently.
|
|
91
|
+
// Region mode intercepts the two legal applyExpr positions at the
|
|
92
|
+
// statement level (if-condition, contains-probe) so their channel span
|
|
93
|
+
// can mark/truncate; reaching here means an unmarkable position.
|
|
94
|
+
if (ctx.annMode || ctx.regionMode) {
|
|
95
|
+
throw new SerializeError(
|
|
96
|
+
"applyExpr in an unmarkable position for keyword '" +
|
|
97
|
+
ctx.currentKeyword +
|
|
98
|
+
"' (" +
|
|
99
|
+
(ctx.annMode ? "annotation" : "region") +
|
|
100
|
+
" mode)",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
// Same call expression an `apply` statement builds; `fold` on this
|
|
104
|
+
// apply is not consulted here (it governs how a wrapping statement
|
|
105
|
+
// uses the value, not how the call itself is rendered).
|
|
106
|
+
return applyCall(ctx, e.apply);
|
|
107
|
+
case "coverageCovers": {
|
|
108
|
+
// Test the folded channel bound by a preceding coverageFold (rule 6):
|
|
109
|
+
// set membership for names, prefix-or-index membership for indexes.
|
|
110
|
+
if (!ctx.regionMode) {
|
|
111
|
+
throw new SerializeError(
|
|
112
|
+
"coverageCovers requires region emission (phase B)",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const fold = ctx.coverageFolds.get(e.fold);
|
|
116
|
+
if (!fold) {
|
|
117
|
+
throw new SerializeError(
|
|
118
|
+
"coverageCovers without a preceding coverageFold",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const target = expr(ctx, e.target);
|
|
122
|
+
if (fold.half === "names") return js`${fold.var}.has(${target})`;
|
|
123
|
+
return js`(${target} < ${fold.var}.coveredPrefix || ${fold.var}.coveredIdx.has(${target}))`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function typeTest(
|
|
129
|
+
ctx: UnitContext,
|
|
130
|
+
target: LowerExpr,
|
|
131
|
+
types: readonly string[],
|
|
132
|
+
): CodeChunk {
|
|
133
|
+
const x = expr(ctx, target);
|
|
134
|
+
const tests = types.map((t) => {
|
|
135
|
+
switch (t) {
|
|
136
|
+
case "object":
|
|
137
|
+
// Inline (no helper call); CSE'd into one guard per unit value —
|
|
138
|
+
// repeated per-property object tests dominated flag-mode profiles.
|
|
139
|
+
if (x.text === ctx.valueVar.text && types.length === 1) {
|
|
140
|
+
return ensureObjGuard(ctx);
|
|
141
|
+
}
|
|
142
|
+
return js`(typeof ${x} === "object" && ${x} !== null && !Array.isArray(${x}))`;
|
|
143
|
+
case "array":
|
|
144
|
+
return js`Array.isArray(${x})`;
|
|
145
|
+
case "null":
|
|
146
|
+
return js`(${x} === null)`;
|
|
147
|
+
case "integer":
|
|
148
|
+
return js`(typeof ${x} === "number" && Number.isInteger(${x}))`;
|
|
149
|
+
case "string":
|
|
150
|
+
case "boolean":
|
|
151
|
+
return js`(typeof ${x} === ${str(t)})`;
|
|
152
|
+
case "number":
|
|
153
|
+
return js`(typeof ${x} === ${str("number")})`;
|
|
154
|
+
default:
|
|
155
|
+
throw new SerializeError("unknown type test '" + t + "'");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return tests.length === 1 ? tests[0]! : js`(${join(" || ", tests)})`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function helperCall(
|
|
162
|
+
ctx: UnitContext,
|
|
163
|
+
helper: string,
|
|
164
|
+
args: readonly LowerExpr[],
|
|
165
|
+
): CodeChunk {
|
|
166
|
+
if (helper === "jsonEqual") {
|
|
167
|
+
const prim = args.find(
|
|
168
|
+
(a) =>
|
|
169
|
+
a.kind === "const" && (a.value === null || typeof a.value !== "object"),
|
|
170
|
+
);
|
|
171
|
+
const other = args.find((a) => a !== prim);
|
|
172
|
+
if (prim && other) {
|
|
173
|
+
return js`(${expr(ctx, other)} === ${expr(ctx, prim)})`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const rendered = args.map((a) => expr(ctx, a));
|
|
177
|
+
const hoisted: Record<string, string> = {
|
|
178
|
+
codePointLength: "h_cpl",
|
|
179
|
+
jsonEqual: "h_eq",
|
|
180
|
+
canonicalKey: "h_ck",
|
|
181
|
+
escapeSegment: "h_esc",
|
|
182
|
+
isMultipleOf: "h_mof",
|
|
183
|
+
hasDuplicateItems: "h_dup",
|
|
184
|
+
firstDuplicatePair: "h_fdp",
|
|
185
|
+
};
|
|
186
|
+
switch (helper) {
|
|
187
|
+
case "keysOf":
|
|
188
|
+
return js`Object.keys(${rendered[0]!})`;
|
|
189
|
+
case "lengthOf":
|
|
190
|
+
return js`${rendered[0]!}.length`;
|
|
191
|
+
case "codePointLength":
|
|
192
|
+
case "jsonEqual":
|
|
193
|
+
case "canonicalKey":
|
|
194
|
+
case "escapeSegment":
|
|
195
|
+
case "isMultipleOf":
|
|
196
|
+
case "hasDuplicateItems":
|
|
197
|
+
case "firstDuplicatePair":
|
|
198
|
+
return js`${id(hoisted[helper]!)}(${join(", ", rendered)})`;
|
|
199
|
+
default:
|
|
200
|
+
throw new SerializeError("helper '" + helper + "' is not supported");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Object-guard CSE: one typeof/null/array test per unit value, shared by an
|
|
2
|
+
// inlined here-cursor child with its parent.
|
|
3
|
+
|
|
4
|
+
import { type CodeChunk, id, js } from "../emit.js";
|
|
5
|
+
import { UnitContext } from "./context.js";
|
|
6
|
+
|
|
7
|
+
/** The CSE'd object-test variable for this unit's own value. */
|
|
8
|
+
export function ensureObjGuard(ctx: UnitContext): CodeChunk {
|
|
9
|
+
if (ctx.guardParent) return ensureObjGuard(ctx.guardParent);
|
|
10
|
+
ctx.objGuardVar ??= id("g" + String(ctx.counters.temp++));
|
|
11
|
+
ctx.objGuardUsed = true;
|
|
12
|
+
return ctx.objGuardVar;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Declaration for the guard, when any statement used it. */
|
|
16
|
+
export function guardDecl(ctx: UnitContext): CodeChunk | null {
|
|
17
|
+
if (ctx.guardParent || !ctx.objGuardUsed || !ctx.objGuardVar) return null;
|
|
18
|
+
const x = ctx.valueVar;
|
|
19
|
+
return js`const ${ctx.objGuardVar} = (typeof ${x} === "object" && ${x} !== null && !Array.isArray(${x}));`;
|
|
20
|
+
}
|