@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,166 @@
|
|
|
1
|
+
// Standalone source emission (D10 build-time mode): a self-contained ES
|
|
2
|
+
// module with zero imports and zero code generation — deployable under a
|
|
3
|
+
// strict CSP (`node --disallow-code-generation-from-strings`, workers,
|
|
4
|
+
// serverless cold start).
|
|
5
|
+
//
|
|
6
|
+
// Scope (M6.5 design call, DESIGN §7): fully static plans only. A schema
|
|
7
|
+
// with dynamic islands or fallback units needs the interpreter at runtime,
|
|
8
|
+
// and the interpreter IS the CSP-safe tier (D10) — under CSP, evaluate such
|
|
9
|
+
// schemas with @json-schema-engine/core directly; where code generation is allowed, use
|
|
10
|
+
// the runtime mode. Emitting a core-importing hybrid module buys nothing
|
|
11
|
+
// over those two paths.
|
|
12
|
+
//
|
|
13
|
+
// The preamble duplicates core's helper semantics by necessity (a
|
|
14
|
+
// self-contained module cannot import them). Drift is guarded twice:
|
|
15
|
+
// standalone.test.ts compares every helper against its core original over a
|
|
16
|
+
// vector corpus, and the CSP check re-runs the official suite through
|
|
17
|
+
// emitted modules.
|
|
18
|
+
import { DEFAULT_MAX_DEPTH } from "@json-schema-engine/core";
|
|
19
|
+
import { buildPlan } from "./plan.js";
|
|
20
|
+
import { serializePlan } from "./serialize/index.js";
|
|
21
|
+
/** Thrown when a plan needs the interpreter at runtime (islands/fallbacks). */
|
|
22
|
+
export class StandaloneUnsupportedError extends Error {
|
|
23
|
+
}
|
|
24
|
+
// Helper preamble: compiler-controlled structural code (never
|
|
25
|
+
// schema-derived), semantics mirrored 1:1 from @json-schema-engine/core — see the drift
|
|
26
|
+
// guards above. h_rx mirrors core's schemaRegExp (u-flag with fallback).
|
|
27
|
+
/**
|
|
28
|
+
* The standalone helper preamble, exported so standalone.test.ts can compare
|
|
29
|
+
* each helper against its core original over a vector corpus (the drift
|
|
30
|
+
* guard the header describes). Not part of the public API surface.
|
|
31
|
+
*/
|
|
32
|
+
export const STANDALONE_PREAMBLE = `// Generated by @json-schema-engine/compiler (standalone mode). Self-contained; no imports.
|
|
33
|
+
const h_obj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34
|
+
const h_int = (v) => typeof v === "number" && Number.isInteger(v);
|
|
35
|
+
const h_hop = Object.prototype.hasOwnProperty;
|
|
36
|
+
const h_s0 = [];
|
|
37
|
+
// Mirrors core's MaxDepthExceededError, which the interpreter and every
|
|
38
|
+
// runtime-compiled artifact throw for this bound. Self-contained emission
|
|
39
|
+
// cannot share core's class object, so \`instanceof\` against it is false
|
|
40
|
+
// here by construction; \`constructor.name\` is the discriminator a
|
|
41
|
+
// standalone consumer has. Throwing RangeError instead would be worse than
|
|
42
|
+
// imprecise: core converts a native stack RangeError INTO this class so a
|
|
43
|
+
// depth failure is never uncatchable by type, and reusing RangeError for
|
|
44
|
+
// the bound makes "schema too deep" and "blew the native stack"
|
|
45
|
+
// indistinguishable.
|
|
46
|
+
class MaxDepthExceededError extends Error {}
|
|
47
|
+
const h_rx = (source) => {
|
|
48
|
+
try {
|
|
49
|
+
return new RegExp(source, "u");
|
|
50
|
+
} catch {
|
|
51
|
+
return new RegExp(source);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const h_eq = (a, b) => {
|
|
55
|
+
if (a === b) return true;
|
|
56
|
+
if (Array.isArray(a)) {
|
|
57
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
58
|
+
for (let i = 0; i < a.length; i++) if (!h_eq(a[i], b[i])) return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (h_obj(a)) {
|
|
62
|
+
if (!h_obj(b)) return false;
|
|
63
|
+
const keys = Object.keys(a);
|
|
64
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
65
|
+
for (const k of keys) {
|
|
66
|
+
if (!h_hop.call(b, k) || !h_eq(a[k], b[k])) return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
const h_ck = (v) => {
|
|
73
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
74
|
+
if (Array.isArray(v)) return "[" + v.map(h_ck).join(",") + "]";
|
|
75
|
+
const keys = Object.keys(v).sort();
|
|
76
|
+
return (
|
|
77
|
+
"{" +
|
|
78
|
+
keys.map((k) => JSON.stringify(k) + ":" + h_ck(v[k])).join(",") +
|
|
79
|
+
"}"
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
const h_dup = (items) => {
|
|
83
|
+
const seen = new Map();
|
|
84
|
+
for (let i = 0; i < items.length; i++) {
|
|
85
|
+
const key = h_ck(items[i]);
|
|
86
|
+
const bucket = seen.get(key);
|
|
87
|
+
if (bucket === undefined) {
|
|
88
|
+
seen.set(key, [i]);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const j of bucket) if (h_eq(items[j], items[i])) return true;
|
|
92
|
+
bucket.push(i);
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
const h_cpl = (s) => {
|
|
97
|
+
let n = 0;
|
|
98
|
+
for (let i = 0; i < s.length; i++) {
|
|
99
|
+
n++;
|
|
100
|
+
const c = s.charCodeAt(i);
|
|
101
|
+
if (c >= 0xd800 && c <= 0xdbff) i++;
|
|
102
|
+
}
|
|
103
|
+
return n;
|
|
104
|
+
};
|
|
105
|
+
const h_esc = (s) => s.replace(/~/g, "~0").replace(/\\//g, "~1");
|
|
106
|
+
const h_digits = (n) => {
|
|
107
|
+
if (!Number.isFinite(n)) return 0;
|
|
108
|
+
const s = Math.abs(n).toString();
|
|
109
|
+
const eIndex = s.indexOf("e");
|
|
110
|
+
if (eIndex !== -1) {
|
|
111
|
+
const mantissa = s.slice(0, eIndex);
|
|
112
|
+
const exponent = Number(s.slice(eIndex + 1));
|
|
113
|
+
const dot = mantissa.indexOf(".");
|
|
114
|
+
const mantissaDigits = dot === -1 ? 0 : mantissa.length - dot - 1;
|
|
115
|
+
return Math.max(0, mantissaDigits - exponent);
|
|
116
|
+
}
|
|
117
|
+
const dot = s.indexOf(".");
|
|
118
|
+
return dot === -1 ? 0 : s.length - dot - 1;
|
|
119
|
+
};
|
|
120
|
+
const h_mof = (instance, divisor) => {
|
|
121
|
+
const scale = 10 ** Math.max(h_digits(instance), h_digits(divisor));
|
|
122
|
+
const scaledInstance = instance * scale;
|
|
123
|
+
const scaledDivisor = divisor * scale;
|
|
124
|
+
if (Number.isFinite(scaledInstance) && Number.isFinite(scaledDivisor)) {
|
|
125
|
+
return Math.round(scaledInstance) % Math.round(scaledDivisor) === 0;
|
|
126
|
+
}
|
|
127
|
+
const quotient = instance / divisor;
|
|
128
|
+
return Number.isFinite(quotient) && Number.isInteger(quotient);
|
|
129
|
+
};
|
|
130
|
+
`;
|
|
131
|
+
/**
|
|
132
|
+
* Emit a self-contained ES module validating against one registered,
|
|
133
|
+
* fully-static schema. The module's default export is
|
|
134
|
+
* `validate(instance): boolean`.
|
|
135
|
+
* @throws StandaloneUnsupportedError when the plan contains interpreted
|
|
136
|
+
* units — use the interpreter (CSP) or runtime compilation (no CSP).
|
|
137
|
+
*/
|
|
138
|
+
export function emitStandalone(engine, schemaUri, options = {}) {
|
|
139
|
+
const plan = buildPlan(engine, schemaUri);
|
|
140
|
+
if (plan.targets.length > 0) {
|
|
141
|
+
const causes = [...new Set(plan.targets.map((t) => t.cause))].join(", ");
|
|
142
|
+
throw new StandaloneUnsupportedError(`schema '${schemaUri}' needs the interpreter at runtime (${causes}); ` +
|
|
143
|
+
"standalone emission covers fully static schemas only");
|
|
144
|
+
}
|
|
145
|
+
// Format assertions resolve through the engine's format table (predicates
|
|
146
|
+
// like IDNA that a zero-import module cannot duplicate); a format-bearing
|
|
147
|
+
// schema stays on the interpreter (CSP) or runtime compilation (no CSP).
|
|
148
|
+
if (plan.formats.length > 0) {
|
|
149
|
+
throw new StandaloneUnsupportedError(`schema '${schemaUri}' asserts formats (${plan.formats.join(", ")}); ` +
|
|
150
|
+
"standalone emission cannot carry the engine's format predicates");
|
|
151
|
+
}
|
|
152
|
+
// A tracked consumer's runtime coverage channel binds the harvest/fold
|
|
153
|
+
// helpers the standalone preamble deliberately omits (phase B is runtime
|
|
154
|
+
// mode only); the interpreter is the CSP-safe tier for these schemas.
|
|
155
|
+
if ([...plan.units.values()].some((u) => u.tracking)) {
|
|
156
|
+
throw new StandaloneUnsupportedError(`schema '${schemaUri}' has a dynamic-coverage consumer needing runtime ` +
|
|
157
|
+
"tracking; standalone emission covers static-coverage schemas only");
|
|
158
|
+
}
|
|
159
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
160
|
+
const depthPreamble = `const h_maxd = ${String(maxDepth)};\n` +
|
|
161
|
+
`const h_deep = () => { throw new MaxDepthExceededError("compiled evaluation exceeds maxDepth (${String(maxDepth)})"); };\n`;
|
|
162
|
+
return (STANDALONE_PREAMBLE +
|
|
163
|
+
depthPreamble +
|
|
164
|
+
serializePlan(plan, engine.registry, { mode: "standalone" }));
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=standalone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standalone.js","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,yEAAyE;AACzE,uEAAuE;AACvE,0BAA0B;AAC1B,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,4EAA4E;AAC5E,wFAAwF;AACxF,yEAAyE;AACzE,wBAAwB;AACxB,EAAE;AACF,kEAAkE;AAClE,qEAAqE;AACrE,4EAA4E;AAC5E,sEAAsE;AACtE,mBAAmB;AAEnB,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,+EAA+E;AAC/E,MAAM,OAAO,0BAA2B,SAAQ,KAAK;CAAG;AAExD,8DAA8D;AAC9D,wFAAwF;AACxF,yEAAyE;AACzE;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkGlC,CAAC;AAQF;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAc,EACd,SAAiB,EACjB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzE,MAAM,IAAI,0BAA0B,CAClC,WAAW,SAAS,uCAAuC,MAAM,KAAK;YACpE,sDAAsD,CACzD,CAAC;IACJ,CAAC;IACD,0EAA0E;IAC1E,0EAA0E;IAC1E,yEAAyE;IACzE,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,0BAA0B,CAClC,WAAW,SAAS,sBAAsB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YACpE,iEAAiE,CACpE,CAAC;IACJ,CAAC;IACD,uEAAuE;IACvE,yEAAyE;IACzE,sEAAsE;IACtE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,0BAA0B,CAClC,WAAW,SAAS,oDAAoD;YACtE,mEAAmE,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACvD,MAAM,aAAa,GACjB,kBAAkB,MAAM,CAAC,QAAQ,CAAC,KAAK;QACvC,iGAAiG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC/H,OAAO,CACL,mBAAmB;QACnB,aAAa;QACb,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAC7D,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@json-schema-engine/compiler",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Compiler tier: emits specialized validators from @json-schema-engine/core registrations, trampolining to the interpreter for dynamic islands (DESIGN.md M6)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"jse-source": "./src/index.ts",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@json-schema-engine/core": "^0.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@json-schema-engine/formats": "^0.0.1"
|
|
26
|
+
},
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/handrews/json-schema-engine.git",
|
|
36
|
+
"directory": "packages/compiler"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/emit.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The gated code serializer (D20/M6): the ONLY module that assembles
|
|
2
|
+
// JavaScript source text. Every value that reaches emitted code passes
|
|
3
|
+
// through a typed wrapper here, so schema-derived data is escaped by
|
|
4
|
+
// construction — raw string concatenation of untrusted input is impossible.
|
|
5
|
+
//
|
|
6
|
+
// The `js` tag accepts only CodeChunk arguments (the branded outputs of the
|
|
7
|
+
// wrappers below); a bare string throws. Template static parts are literals
|
|
8
|
+
// by grammar. Downstream modules traffic in CodeChunk, never string, so the
|
|
9
|
+
// type system carries the guarantee and ESLint fences the one escape hatch.
|
|
10
|
+
|
|
11
|
+
import type { JsonValue } from "@json-schema-engine/core";
|
|
12
|
+
|
|
13
|
+
const BRAND = Symbol("CodeChunk");
|
|
14
|
+
|
|
15
|
+
/** An opaque, already-escaped fragment of emitted JavaScript. */
|
|
16
|
+
export interface CodeChunk {
|
|
17
|
+
readonly [BRAND]: true;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const chunk = (text: string): CodeChunk => ({ [BRAND]: true, text });
|
|
22
|
+
|
|
23
|
+
const isChunk = (v: unknown): v is CodeChunk =>
|
|
24
|
+
typeof v === "object" && v !== null && BRAND in v;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tagged template for emitted code. Interpolations MUST be CodeChunks (from
|
|
28
|
+
* {@link id}/{@link str}/{@link num}/{@link json}/{@link regexTest}/{@link raw}/
|
|
29
|
+
* {@link frag}); anything else throws, so a stray schema string can never be
|
|
30
|
+
* spliced verbatim.
|
|
31
|
+
*/
|
|
32
|
+
export function js(
|
|
33
|
+
strings: TemplateStringsArray,
|
|
34
|
+
...parts: readonly CodeChunk[]
|
|
35
|
+
): CodeChunk {
|
|
36
|
+
let out = strings[0]!;
|
|
37
|
+
for (let i = 0; i < parts.length; i++) {
|
|
38
|
+
const part = parts[i];
|
|
39
|
+
if (!isChunk(part)) {
|
|
40
|
+
throw new TypeError(
|
|
41
|
+
`js\`\` interpolation ${i} is not a CodeChunk; wrap it (id/str/num/json/…)`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
out += part.text + strings[i + 1]!;
|
|
45
|
+
}
|
|
46
|
+
return chunk(out);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
50
|
+
const RESERVED = new Set([
|
|
51
|
+
"break",
|
|
52
|
+
"case",
|
|
53
|
+
"catch",
|
|
54
|
+
"class",
|
|
55
|
+
"const",
|
|
56
|
+
"continue",
|
|
57
|
+
"debugger",
|
|
58
|
+
"default",
|
|
59
|
+
"delete",
|
|
60
|
+
"do",
|
|
61
|
+
"else",
|
|
62
|
+
"export",
|
|
63
|
+
"extends",
|
|
64
|
+
"false",
|
|
65
|
+
"finally",
|
|
66
|
+
"for",
|
|
67
|
+
"function",
|
|
68
|
+
"if",
|
|
69
|
+
"import",
|
|
70
|
+
"in",
|
|
71
|
+
"instanceof",
|
|
72
|
+
"new",
|
|
73
|
+
"null",
|
|
74
|
+
"return",
|
|
75
|
+
"super",
|
|
76
|
+
"switch",
|
|
77
|
+
"this",
|
|
78
|
+
"throw",
|
|
79
|
+
"true",
|
|
80
|
+
"try",
|
|
81
|
+
"typeof",
|
|
82
|
+
"var",
|
|
83
|
+
"void",
|
|
84
|
+
"while",
|
|
85
|
+
"with",
|
|
86
|
+
"yield",
|
|
87
|
+
"let",
|
|
88
|
+
"static",
|
|
89
|
+
"enum",
|
|
90
|
+
"await",
|
|
91
|
+
"implements",
|
|
92
|
+
"package",
|
|
93
|
+
"protected",
|
|
94
|
+
"interface",
|
|
95
|
+
"private",
|
|
96
|
+
"public",
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A machine-generated identifier. NEVER pass schema-derived text here —
|
|
101
|
+
* identifiers come from the compiler's own counters and a fixed helper
|
|
102
|
+
* vocabulary. Rejects anything that is not a plain identifier or is reserved.
|
|
103
|
+
*/
|
|
104
|
+
export function id(name: string): CodeChunk {
|
|
105
|
+
if (!IDENT.test(name) || RESERVED.has(name)) {
|
|
106
|
+
throw new TypeError(`unsafe identifier '${name}'`);
|
|
107
|
+
}
|
|
108
|
+
return chunk(name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A JavaScript string literal, escaped for embedding (incl. U+2028/U+2029). */
|
|
112
|
+
export function str(value: string): CodeChunk {
|
|
113
|
+
const json = JSON.stringify(value);
|
|
114
|
+
// JSON.stringify leaves U+2028/U+2029 literal; they are valid in JSON but
|
|
115
|
+
// were line terminators in pre-ES2019 JS and still trip some tooling.
|
|
116
|
+
return chunk(
|
|
117
|
+
json.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A numeric literal, JSON-sourced (finite), with an explicit -0 branch. */
|
|
122
|
+
export function num(value: number): CodeChunk {
|
|
123
|
+
if (!Number.isFinite(value)) {
|
|
124
|
+
throw new TypeError(`non-finite numeric constant ${String(value)}`);
|
|
125
|
+
}
|
|
126
|
+
if (Object.is(value, -0)) return chunk("-0");
|
|
127
|
+
return chunk(String(value));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True when `value` or any nested value is an object with a "__proto__" own key. */
|
|
131
|
+
function hasProtoKey(value: JsonValue): boolean {
|
|
132
|
+
if (value === null || typeof value !== "object") return false;
|
|
133
|
+
if (Array.isArray(value)) return value.some(hasProtoKey);
|
|
134
|
+
if (Object.hasOwn(value, "__proto__")) return true;
|
|
135
|
+
return Object.values(value).some(hasProtoKey);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A JSON constant. Emits an object/array literal only when no nested own key
|
|
140
|
+
* is `__proto__` (a literal `{"__proto__": …}` sets the prototype in source,
|
|
141
|
+
* unlike JSON.parse); otherwise routes through a JSON.parse call so the
|
|
142
|
+
* prototype is never touched.
|
|
143
|
+
*/
|
|
144
|
+
export function json(value: JsonValue): CodeChunk {
|
|
145
|
+
if (value === null || typeof value !== "object") {
|
|
146
|
+
if (typeof value === "number") return num(value);
|
|
147
|
+
if (typeof value === "string") return str(value);
|
|
148
|
+
return chunk(String(value)); // boolean
|
|
149
|
+
}
|
|
150
|
+
if (hasProtoKey(value)) {
|
|
151
|
+
return chunk(`JSON.parse(${str(JSON.stringify(value)).text})`);
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
return chunk(`[${value.map((v) => json(v).text).join(",")}]`);
|
|
155
|
+
}
|
|
156
|
+
const members = Object.entries(value).map(
|
|
157
|
+
([k, v]) => `${str(k).text}:${json(v).text}`,
|
|
158
|
+
);
|
|
159
|
+
return chunk(`{${members.join(",")}}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A regex membership test against a hoisted, pre-compiled pattern. The
|
|
164
|
+
* pattern source is escaped as a string literal; the compiled RegExp is
|
|
165
|
+
* supplied at runtime through the artifact's regex table (never
|
|
166
|
+
* `RegExp.prototype.toString`, never inline `/…/`).
|
|
167
|
+
*/
|
|
168
|
+
export function regexTest(
|
|
169
|
+
tableRef: CodeChunk,
|
|
170
|
+
source: string,
|
|
171
|
+
target: CodeChunk,
|
|
172
|
+
): CodeChunk {
|
|
173
|
+
return chunk(`${tableRef.text}[${str(source).text}].test(${target.text})`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Splice previously built chunks (already escaped). */
|
|
177
|
+
export function frag(...chunks: readonly CodeChunk[]): CodeChunk {
|
|
178
|
+
return chunk(chunks.map((c) => c.text).join(""));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Join chunks with a separator (both already escaped). */
|
|
182
|
+
export function join(sep: string, chunks: readonly CodeChunk[]): CodeChunk {
|
|
183
|
+
return chunk(chunks.map((c) => c.text).join(sep));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Internal escape hatch for structural code the compiler itself controls
|
|
188
|
+
* (keywords, operators) — NOT for schema-derived data. Guarded by ESLint so
|
|
189
|
+
* only emit-internal call sites use it. Kept explicit so audits grep one name.
|
|
190
|
+
*/
|
|
191
|
+
export function raw(controlledSource: string): CodeChunk {
|
|
192
|
+
return chunk(controlledSource);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The assembled source text of a chunk. */
|
|
196
|
+
export const render = (c: CodeChunk): string => c.text;
|
package/src/explain.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Compilation-plan diagnostics (deferred-register item, built for the
|
|
2
|
+
// plan-census gates): interpreted fallback is always CORRECT, so a
|
|
3
|
+
// regression that silently flips units static→interpreted passes every
|
|
4
|
+
// suite/differential/fuzz gate — only a census over these summaries
|
|
5
|
+
// notices. The summarizer is pure read-only projection of plan data.
|
|
6
|
+
|
|
7
|
+
import type { CompilationPlan, FallbackCause } from "./plan.js";
|
|
8
|
+
|
|
9
|
+
/** Aggregate view of one {@link CompilationPlan}'s unit classification. */
|
|
10
|
+
export interface CompilationExplanation {
|
|
11
|
+
/** Every planned unit, static or not. */
|
|
12
|
+
totalUnits: number;
|
|
13
|
+
staticUnits: number;
|
|
14
|
+
interpretedUnits: number;
|
|
15
|
+
/** Interpreted-unit count per fallback cause; absent cause = zero. */
|
|
16
|
+
causes: Partial<Record<FallbackCause, number>>;
|
|
17
|
+
/**
|
|
18
|
+
* Canonical `baseUri#pointer` keys of the interpreted units — for
|
|
19
|
+
* diagnosis when a census count assertion fails, not for pinning (key
|
|
20
|
+
* lists churn on suite reorganization without adding regression signal).
|
|
21
|
+
*/
|
|
22
|
+
interpretedKeys: string[];
|
|
23
|
+
/** Units whose apply paths can reach an interpreted unit. */
|
|
24
|
+
reachesInterpretedCount: number;
|
|
25
|
+
/**
|
|
26
|
+
* Static consumer units compiled with runtime evaluated-set tracking (phase
|
|
27
|
+
* B) — the flag-mode alternative to interpreting a dynamic-coverage consumer.
|
|
28
|
+
*/
|
|
29
|
+
trackingUnits: number;
|
|
30
|
+
/** Static units in some tracked unit's in-place coverage region (phase B). */
|
|
31
|
+
regionUnits: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Summarizes a plan's classification for census gates and diagnostics. */
|
|
35
|
+
export function explainCompilation(
|
|
36
|
+
plan: CompilationPlan,
|
|
37
|
+
): CompilationExplanation {
|
|
38
|
+
const causes: Partial<Record<FallbackCause, number>> = {};
|
|
39
|
+
const interpretedKeys: string[] = [];
|
|
40
|
+
let staticUnits = 0;
|
|
41
|
+
let reachesInterpretedCount = 0;
|
|
42
|
+
let trackingUnits = 0;
|
|
43
|
+
let regionUnits = 0;
|
|
44
|
+
for (const unit of plan.units.values()) {
|
|
45
|
+
if (unit.kind === "static") staticUnits++;
|
|
46
|
+
else {
|
|
47
|
+
interpretedKeys.push(unit.key);
|
|
48
|
+
if (unit.cause !== undefined) {
|
|
49
|
+
causes[unit.cause] = (causes[unit.cause] ?? 0) + 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (unit.reachesInterpreted) reachesInterpretedCount++;
|
|
53
|
+
if (unit.tracking) trackingUnits++;
|
|
54
|
+
if (unit.inRegion) regionUnits++;
|
|
55
|
+
}
|
|
56
|
+
interpretedKeys.sort();
|
|
57
|
+
return {
|
|
58
|
+
totalUnits: plan.units.size,
|
|
59
|
+
staticUnits,
|
|
60
|
+
interpretedUnits: plan.units.size - staticUnits,
|
|
61
|
+
causes,
|
|
62
|
+
interpretedKeys,
|
|
63
|
+
reachesInterpretedCount,
|
|
64
|
+
trackingUnits,
|
|
65
|
+
regionUnits,
|
|
66
|
+
};
|
|
67
|
+
}
|