@zmdb/compiler 1.0.0-beta.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 (172) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +40 -0
  3. package/dist/codegen/index.d.ts +50 -0
  4. package/dist/codegen/index.d.ts.map +1 -0
  5. package/dist/codegen/index.js +364 -0
  6. package/dist/codegen/index.js.map +1 -0
  7. package/dist/codegen/scan.d.ts +105 -0
  8. package/dist/codegen/scan.d.ts.map +1 -0
  9. package/dist/codegen/scan.js +450 -0
  10. package/dist/codegen/scan.js.map +1 -0
  11. package/dist/codegen/witness.d.ts +76 -0
  12. package/dist/codegen/witness.d.ts.map +1 -0
  13. package/dist/codegen/witness.js +690 -0
  14. package/dist/codegen/witness.js.map +1 -0
  15. package/dist/config/contract.d.ts +41 -0
  16. package/dist/config/contract.d.ts.map +1 -0
  17. package/dist/config/contract.js +5 -0
  18. package/dist/config/contract.js.map +1 -0
  19. package/dist/config/index.d.ts +53 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +277 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/index.zmdb.generated.d.ts +12 -0
  24. package/dist/config/index.zmdb.generated.js +28 -0
  25. package/dist/config/index.zmdb.witness.d.ts +3 -0
  26. package/dist/config/index.zmdb.witness.d.ts.map +1 -0
  27. package/dist/config/index.zmdb.witness.js +20 -0
  28. package/dist/config/index.zmdb.witness.js.map +1 -0
  29. package/dist/configured-plugin.d.ts +11 -0
  30. package/dist/configured-plugin.d.ts.map +1 -0
  31. package/dist/configured-plugin.js +27 -0
  32. package/dist/configured-plugin.js.map +1 -0
  33. package/dist/emit/index.d.ts +103 -0
  34. package/dist/emit/index.d.ts.map +1 -0
  35. package/dist/emit/index.js +1356 -0
  36. package/dist/emit/index.js.map +1 -0
  37. package/dist/errors.d.ts +7 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +2 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +46 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +254 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lint/ast.d.ts +17 -0
  46. package/dist/lint/ast.d.ts.map +1 -0
  47. package/dist/lint/ast.js +77 -0
  48. package/dist/lint/ast.js.map +1 -0
  49. package/dist/lint/host-types.d.ts +9 -0
  50. package/dist/lint/host-types.d.ts.map +1 -0
  51. package/dist/lint/host-types.js +2 -0
  52. package/dist/lint/host-types.js.map +1 -0
  53. package/dist/lint/index.d.ts +23 -0
  54. package/dist/lint/index.d.ts.map +1 -0
  55. package/dist/lint/index.js +53 -0
  56. package/dist/lint/index.js.map +1 -0
  57. package/dist/lint/rules/no-distributed-nullable-tags.d.ts +3 -0
  58. package/dist/lint/rules/no-distributed-nullable-tags.d.ts.map +1 -0
  59. package/dist/lint/rules/no-distributed-nullable-tags.js +71 -0
  60. package/dist/lint/rules/no-distributed-nullable-tags.js.map +1 -0
  61. package/dist/lint/rules/no-empty-patch.d.ts +3 -0
  62. package/dist/lint/rules/no-empty-patch.d.ts.map +1 -0
  63. package/dist/lint/rules/no-empty-patch.js +26 -0
  64. package/dist/lint/rules/no-empty-patch.js.map +1 -0
  65. package/dist/lint/rules/no-interpolated-sql.d.ts +3 -0
  66. package/dist/lint/rules/no-interpolated-sql.d.ts.map +1 -0
  67. package/dist/lint/rules/no-interpolated-sql.js +35 -0
  68. package/dist/lint/rules/no-interpolated-sql.js.map +1 -0
  69. package/dist/lint/rules/no-unbounded-find.d.ts +3 -0
  70. package/dist/lint/rules/no-unbounded-find.d.ts.map +1 -0
  71. package/dist/lint/rules/no-unbounded-find.js +26 -0
  72. package/dist/lint/rules/no-unbounded-find.js.map +1 -0
  73. package/dist/lint/rules/no-unknown-json-column.d.ts +3 -0
  74. package/dist/lint/rules/no-unknown-json-column.d.ts.map +1 -0
  75. package/dist/lint/rules/no-unknown-json-column.js +35 -0
  76. package/dist/lint/rules/no-unknown-json-column.js.map +1 -0
  77. package/dist/lint/rules/require-sql-on-number.d.ts +3 -0
  78. package/dist/lint/rules/require-sql-on-number.d.ts.map +1 -0
  79. package/dist/lint/rules/require-sql-on-number.js +34 -0
  80. package/dist/lint/rules/require-sql-on-number.js.map +1 -0
  81. package/dist/lint/types.d.ts +12 -0
  82. package/dist/lint/types.d.ts.map +1 -0
  83. package/dist/lint/types.js +2 -0
  84. package/dist/lint/types.js.map +1 -0
  85. package/dist/metro/metro.d.ts +22 -0
  86. package/dist/metro/metro.d.ts.map +1 -0
  87. package/dist/metro/metro.js +242 -0
  88. package/dist/metro/metro.js.map +1 -0
  89. package/dist/protobuf/decode.d.ts +20 -0
  90. package/dist/protobuf/decode.d.ts.map +1 -0
  91. package/dist/protobuf/decode.js +289 -0
  92. package/dist/protobuf/decode.js.map +1 -0
  93. package/dist/protobuf/descriptor.d.ts +16 -0
  94. package/dist/protobuf/descriptor.d.ts.map +1 -0
  95. package/dist/protobuf/descriptor.js +303 -0
  96. package/dist/protobuf/descriptor.js.map +1 -0
  97. package/dist/protobuf/encode.d.ts +20 -0
  98. package/dist/protobuf/encode.d.ts.map +1 -0
  99. package/dist/protobuf/encode.js +203 -0
  100. package/dist/protobuf/encode.js.map +1 -0
  101. package/dist/protobuf/grpc-ir.d.ts +16 -0
  102. package/dist/protobuf/grpc-ir.d.ts.map +1 -0
  103. package/dist/protobuf/grpc-ir.js +2 -0
  104. package/dist/protobuf/grpc-ir.js.map +1 -0
  105. package/dist/protobuf/plan.d.ts +42 -0
  106. package/dist/protobuf/plan.d.ts.map +1 -0
  107. package/dist/protobuf/plan.js +158 -0
  108. package/dist/protobuf/plan.js.map +1 -0
  109. package/dist/reflect/callsites.d.ts +40 -0
  110. package/dist/reflect/callsites.d.ts.map +1 -0
  111. package/dist/reflect/callsites.js +153 -0
  112. package/dist/reflect/callsites.js.map +1 -0
  113. package/dist/reflect/index.d.ts +106 -0
  114. package/dist/reflect/index.d.ts.map +1 -0
  115. package/dist/reflect/index.js +1421 -0
  116. package/dist/reflect/index.js.map +1 -0
  117. package/dist/reflect/session.d.ts +118 -0
  118. package/dist/reflect/session.d.ts.map +1 -0
  119. package/dist/reflect/session.js +228 -0
  120. package/dist/reflect/session.js.map +1 -0
  121. package/dist/testing/index.d.ts +69 -0
  122. package/dist/testing/index.d.ts.map +1 -0
  123. package/dist/testing/index.js +210 -0
  124. package/dist/testing/index.js.map +1 -0
  125. package/dist/transform/index.d.ts +74 -0
  126. package/dist/transform/index.d.ts.map +1 -0
  127. package/dist/transform/index.js +572 -0
  128. package/dist/transform/index.js.map +1 -0
  129. package/dist/unplugin/index.d.ts +53 -0
  130. package/dist/unplugin/index.d.ts.map +1 -0
  131. package/dist/unplugin/index.js +130 -0
  132. package/dist/unplugin/index.js.map +1 -0
  133. package/dist/unplugin/inline-bench.d.ts +29 -0
  134. package/dist/unplugin/inline-bench.d.ts.map +1 -0
  135. package/dist/unplugin/inline-bench.js +82 -0
  136. package/dist/unplugin/inline-bench.js.map +1 -0
  137. package/package.json +110 -0
  138. package/src/codegen/index.ts +451 -0
  139. package/src/codegen/scan.ts +574 -0
  140. package/src/codegen/witness.ts +819 -0
  141. package/src/config/contract.ts +48 -0
  142. package/src/config/index.ts +384 -0
  143. package/src/config/index.zmdb.generated.d.ts +12 -0
  144. package/src/config/index.zmdb.generated.js +28 -0
  145. package/src/config/index.zmdb.witness.ts +22 -0
  146. package/src/configured-plugin.ts +35 -0
  147. package/src/emit/index.ts +1496 -0
  148. package/src/errors.ts +6 -0
  149. package/src/index.ts +336 -0
  150. package/src/lint/ast.ts +96 -0
  151. package/src/lint/host-types.ts +9 -0
  152. package/src/lint/index.ts +80 -0
  153. package/src/lint/rules/no-distributed-nullable-tags.ts +76 -0
  154. package/src/lint/rules/no-empty-patch.ts +26 -0
  155. package/src/lint/rules/no-interpolated-sql.ts +45 -0
  156. package/src/lint/rules/no-unbounded-find.ts +26 -0
  157. package/src/lint/rules/no-unknown-json-column.ts +36 -0
  158. package/src/lint/rules/require-sql-on-number.ts +34 -0
  159. package/src/lint/types.ts +11 -0
  160. package/src/metro/metro.ts +313 -0
  161. package/src/protobuf/decode.ts +375 -0
  162. package/src/protobuf/descriptor.ts +353 -0
  163. package/src/protobuf/encode.ts +277 -0
  164. package/src/protobuf/grpc-ir.ts +17 -0
  165. package/src/protobuf/plan.ts +244 -0
  166. package/src/reflect/callsites.ts +198 -0
  167. package/src/reflect/index.ts +1723 -0
  168. package/src/reflect/session.ts +276 -0
  169. package/src/testing/index.ts +298 -0
  170. package/src/transform/index.ts +690 -0
  171. package/src/unplugin/index.ts +182 -0
  172. package/src/unplugin/inline-bench.ts +92 -0
@@ -0,0 +1,1356 @@
1
+ // IR → JavaScript. The back-end that makes `is<T>(x)` cost a few `typeof`s.
2
+ //
3
+ // Four targets, one walk each, sharing one naming/hoisting/budget context:
4
+ //
5
+ // check `(v) => boolean` `is<T>`, and the first pass of the rest
6
+ // excess statements that `return false` the strict half of `equals`/`assertEquals`
7
+ // issues `(v, path, out) => void` `assert<T>` / `validate<T>`
8
+ // sample an expression producing a value `random<T>`
9
+ //
10
+ // And two that are not walks, and so are not `EmitTarget`s: `emitJsonSchema` emits a
11
+ // *finished* JSON Schema document for `toJsonSchema<T>()`, and `emitSchemaValue` a
12
+ // finished `CoreSchema` for `schemaOf<T>()`. The other four emit code that runs later and
13
+ // therefore need a walk per target; these two are data, computed here by the very
14
+ // functions — `jsonSchemaFromShape`, `schemaFromIR` — that the value path calls.
15
+ //
16
+ // Three decisions shape all of it.
17
+ //
18
+ // **REQ-AV-7 — no allocation on the success path.** `assert<T>(x)` does not build an
19
+ // issues array and then check whether it is empty. It runs the allocation-free `check`
20
+ // and only walks `issues` once it already knows the value is bad. Valid input therefore
21
+ // allocates nothing at all, and the second walk is paid for exactly where a throw was
22
+ // about to happen anyway.
23
+ //
24
+ // **REQ-AV-4 — the emitted and the runtime paths must agree.** They are two walks, so
25
+ // everything both of them decide — issue text, whether a union has a discriminant —
26
+ // lives in `@zmdb/schema/ir` and is imported by both. The validator runtime's
27
+ // `utilities/index.ts` is the other walk.
28
+ //
29
+ // **Anonymous inlines, named hoists.** A name is the signal that a type may recur or
30
+ // appear twice, and `RefIR` exists because it does. So `is<{ n: number }>(x)` stays a
31
+ // straight-line expression with no call in it, while `is<User>(x)` gets one hoisted
32
+ // `_zmdbCheckUser0` that a `ref` can call. Arrays always hoist: a loop is not an
33
+ // expression, and `.every(cb)` allocates a closure per call.
34
+ //
35
+ // An `unsupported` node is a build error, never a guess (plan D4). The walk records a
36
+ // diagnostic and returns `undefined`, and the transformer leaves that call site alone.
37
+ import { toolSchemaForProvider, ToolSpecRefusalError } from '@zmdb/ai/compiler';
38
+ import { discriminantOf, expectedForConstraint, expectedForDiscriminant, expectedOf, hasExcessCheck, jsonSchemaFromShape, schemaFromIR, shapeOfVariant, } from '@zmdb/schema/ir';
39
+ import { validatePatternComplexity } from '@zmdb/validator';
40
+ import { emitProtoDecoder } from '../protobuf/decode.js';
41
+ import { emitGrpcDescriptor, emitProtoDescriptor } from '../protobuf/descriptor.js';
42
+ import { emitProtoEncoder } from '../protobuf/encode.js';
43
+ /** Sanitise a pattern before it goes between `/` delimiters in emitted source. */
44
+ export function escapePattern(pattern) {
45
+ return pattern
46
+ .replace(/(?<!\\)(?:\\\\)*\//g, match => match.slice(0, -1) + '\\/')
47
+ .replaceAll('\n', '\\n')
48
+ .replaceAll('\r', '\\r')
49
+ .replaceAll('
', '\\u2028')
50
+ .replaceAll('
', '\\u2029');
51
+ }
52
+ const DEFAULT_MAX_HELPERS = 512;
53
+ /** Above this many literal members a hoisted `Set` beats a chain of `===`. */
54
+ const MANY_LITERALS = 8;
55
+ /** An identifier or dotted path is cheap to re-read; anything else is bound once. */
56
+ const SIMPLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
57
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
58
+ const STRING_LITERAL = /^"(?:[^"\\]|\\.)*"$/;
59
+ const NUMERIC = /^\d+$/;
60
+ /** An excess walk that is nothing but a delegation, so the delegation can be dropped. */
61
+ const SOLE_GUARD = /^if \(!(\w+)\(_v\)\) return false;$/;
62
+ function childDepth(depth) {
63
+ return depth === undefined ? undefined : Math.max(0, depth - 1);
64
+ }
65
+ function depthKey(depth) {
66
+ return depth === undefined ? '' : `:${depth}`;
67
+ }
68
+ /**
69
+ * "This is a keyed object." An array is excluded, because the runtime walker has always
70
+ * excluded it, and `is<{}>([])` answering `true` in a built bundle and `false` in dev is
71
+ * exactly the divergence REQ-AV-4 forbids.
72
+ */
73
+ function recordTest(v) {
74
+ return `typeof ${v} === "object" && ${v} !== null && !Array.isArray(${v})`;
75
+ }
76
+ /** Looser: only enough to make a `for…in` or an indexed loop safe. */
77
+ function keyableTest(v) {
78
+ return `typeof ${v} === "object" && ${v} !== null`;
79
+ }
80
+ export class Emitter {
81
+ #prefix;
82
+ #maxHelpers;
83
+ #errorModule;
84
+ #protobufModule;
85
+ #helpers = [];
86
+ #diagnostics = [];
87
+ /** `target:name` → helper, for the emission in progress. Lets a `ref` resolve. */
88
+ #open = new Map();
89
+ /** Named objects reached in this walk, so a shallow `ref` can emit a smaller helper. */
90
+ #definitions = new Map();
91
+ /** `target:fingerprint` → helper, for the whole file. Two call sites share one. */
92
+ #shared = new Map();
93
+ #counter = 0;
94
+ #needsAssertError = false;
95
+ #needsProtoReader = false;
96
+ #needsProtoWriter = false;
97
+ #hasIssueHelper = false;
98
+ #hasFreeze = false;
99
+ #hasIntSample = false;
100
+ #hasStringSample = false;
101
+ constructor(options = {}) {
102
+ this.#prefix = options.prefix ?? '_zmdb';
103
+ this.#maxHelpers = options.maxHelpers ?? DEFAULT_MAX_HELPERS;
104
+ this.#errorModule = options.errorModule ?? '@zmdb/validator/errors';
105
+ this.#protobufModule = options.protobufModule ?? '@zmdb/protobuf/wire';
106
+ }
107
+ get diagnostics() {
108
+ return this.#diagnostics;
109
+ }
110
+ /** Record a build-time refusal discovered at the call-site boundary. */
111
+ refuse(path, reason) {
112
+ this.#diagnostics.push({ path, reason });
113
+ return undefined;
114
+ }
115
+ /** True once anything has been hoisted, so a caller knows a prelude is needed. */
116
+ get hasPrelude() {
117
+ return this.#needsAssertError || this.#needsProtoReader || this.#needsProtoWriter || this.#helpers.length > 0;
118
+ }
119
+ /**
120
+ * Everything that has to sit at the top of the module: the `AssertError` import when a
121
+ * throwing form was emitted, then the helpers in definition order.
122
+ */
123
+ prelude() {
124
+ const lines = [];
125
+ if (this.#needsAssertError) {
126
+ lines.push(`import { AssertError as ${this.#prefix}AssertError } from ${JSON.stringify(this.#errorModule)};`);
127
+ }
128
+ if (this.#needsProtoReader) {
129
+ lines.push(`import { ProtoReader as ${this.#prefix}ProtoReader } from ${JSON.stringify(this.#protobufModule)};`);
130
+ }
131
+ if (this.#needsProtoWriter) {
132
+ lines.push(`import { ProtoWriter as ${this.#prefix}ProtoWriter } from ${JSON.stringify(this.#protobufModule)};`);
133
+ }
134
+ for (const helper of this.#helpers)
135
+ if (helper !== undefined)
136
+ lines.push(helper);
137
+ return lines.join('\n');
138
+ }
139
+ // -------------------------------------------------------------------------
140
+ // The call sites
141
+ // -------------------------------------------------------------------------
142
+ /** `is<T>(expr)` → a boolean expression. */
143
+ emitIs(node, expr, maxDepth) {
144
+ this.#open.clear();
145
+ const depth = this.#depthFor(node, maxDepth);
146
+ const bound = this.#bind(expr);
147
+ const check = this.#check(node, bound.ref, '', depth);
148
+ if (check === undefined)
149
+ return undefined;
150
+ return bound.expression(`(${check})`);
151
+ }
152
+ /** `equals<T>(expr)` → `is<T>` plus a recursive no-excess-keys check. */
153
+ emitEquals(node, expr) {
154
+ this.#open.clear();
155
+ this.#definitions.clear();
156
+ const bound = this.#bind(expr);
157
+ const check = this.#check(node, bound.ref, '');
158
+ if (check === undefined)
159
+ return undefined;
160
+ // The excess walk goes through a hoisted helper rather than inline statements so the
161
+ // whole thing stays one expression: `equals<T>(x)` in a condition should not have to
162
+ // pay for an IIFE just to run a second pass.
163
+ const excess = this.#excessHelper(node);
164
+ if (excess === undefined)
165
+ return undefined;
166
+ if (excess === null)
167
+ return bound.expression(`(${check})`);
168
+ return bound.expression(`((${check}) && ${excess}(${bound.ref}))`);
169
+ }
170
+ /** `assert<T>(expr)` / `assertEquals<T>(expr)` → the value, or a throw. */
171
+ emitAssert(node, expr, strict, maxDepth) {
172
+ const plan = this.#twoPass(node, expr, strict, maxDepth);
173
+ if (!plan)
174
+ return undefined;
175
+ this.#needsAssertError = true;
176
+ // The success path is `if (gate) return v` and nothing else: no array, no issue
177
+ // objects, no closure (REQ-AV-7). Everything below the `if` runs once, on the way to
178
+ // a throw that was going to be expensive regardless.
179
+ return plan.bound.block([
180
+ `if (${plan.gate}) return ${plan.bound.ref};`,
181
+ `const _e = []; ${plan.collect}(${plan.bound.ref}, "input", _e);`,
182
+ `throw new ${this.#prefix}AssertError(_e[0] ? _e[0].message : "validation failed", _e);`,
183
+ ]);
184
+ }
185
+ /** `validate<T>(expr)` → a `ValidateResult<T>`, never a throw. */
186
+ emitValidate(node, expr, strict = false, maxDepth) {
187
+ const plan = this.#twoPass(node, expr, strict, maxDepth);
188
+ if (!plan)
189
+ return undefined;
190
+ return plan.bound.block([
191
+ `if (${plan.gate}) return { success: true, data: ${plan.bound.ref} };`,
192
+ `const _e = []; ${plan.collect}(${plan.bound.ref}, "input", _e);`,
193
+ 'return { success: false, errors: _e };',
194
+ ]);
195
+ }
196
+ /** `random<T>()` → an expression producing a value that satisfies `T`. */
197
+ emitRandom(node) {
198
+ this.#open.clear();
199
+ this.#definitions.clear();
200
+ const sample = this.#sample(node, '');
201
+ return sample === undefined ? undefined : `(${sample})`;
202
+ }
203
+ /**
204
+ * `toJsonSchema<T>()` → a reference to the document, hoisted and frozen (REQ-TF-7).
205
+ *
206
+ * One of the two targets that is not a walk. The other four emit code that runs later;
207
+ * a JSON Schema document is *finished* at build time, so what gets emitted is the
208
+ * answer itself. `jsonSchemaFromShape` is the same function the value path calls, which
209
+ * is what makes the two documents identical rather than merely tested for equality.
210
+ */
211
+ emitJsonSchema(shape) {
212
+ return this.#literal('jsonSchema', 'JsonSchema', jsonSchemaFromShape(shape));
213
+ }
214
+ /**
215
+ * `schemaOf<T>()` → a reference to the generated schema value, hoisted and frozen
216
+ * (REQ-TF-10).
217
+ *
218
+ * The other half of "one IR, several back-ends": the query compiler and the DDL emitter
219
+ * want the table and the column types as data, so what gets emitted is the projection
220
+ * `schemaFromIR` builds — and that includes the IR itself, on the value's `ir` field.
221
+ * Both are in the literal on purpose. The projection is what the query compiler reads on
222
+ * every call and wants flat; the IR is what the decoders, the wire codecs and the OpenAPI
223
+ * document read, and it says things no column map can hold. Emitting only the projection
224
+ * would mean recovering the rest by inference at runtime, which is the walk this design
225
+ * removed.
226
+ *
227
+ * Relations do not travel with the projection. A `CoreSchema` has no relation map — the
228
+ * repository takes one separately — so a `ManyToOne<…>` on the declaration is read, is
229
+ * not a column, and reaches a consumer only through `ir.relations`.
230
+ */
231
+ emitSchemaValue(ir) {
232
+ return this.#literal('schema', 'Schema', schemaFromIR(ir));
233
+ }
234
+ /**
235
+ * `toolFor<T>(provider, name, opts)` → the provider's frozen parameter document.
236
+ *
237
+ * The transformer supplies the framing expressions because `name` and `description`
238
+ * belong to the call site. The schema itself is complete here: a pure projection of the
239
+ * reflected table IR, shared with the runtime schema-value overload.
240
+ */
241
+ emitToolSchema(ir, provider) {
242
+ try {
243
+ const document = toolSchemaForProvider(provider, shapeOfVariant(ir, 'create'));
244
+ return this.#literal('toolSchema', 'ToolSchema', document);
245
+ }
246
+ catch (error) {
247
+ if (!(error instanceof ToolSpecRefusalError))
248
+ throw error;
249
+ const { refusal } = error;
250
+ this.#diagnostics.push({
251
+ path: refusal.path,
252
+ reason: `${refusal.provider} refuses ${refusal.construct}: ${refusal.reason}. ` + refusal.suggestion,
253
+ });
254
+ return undefined;
255
+ }
256
+ }
257
+ /** `protoDescriptor<T>()` -> one compile-time-produced proto3 string literal. */
258
+ emitProtoDescriptor(node, name) {
259
+ const result = emitProtoDescriptor(node, name);
260
+ for (const diagnostic of result.diagnostics)
261
+ this.#diagnostics.push(diagnostic);
262
+ return result.source === undefined ? undefined : JSON.stringify(result.source);
263
+ }
264
+ /** `grpcDescriptor<S>(service, package)` -> one complete proto3 file. */
265
+ emitGrpcDescriptor(node, service, pkg) {
266
+ const result = emitGrpcDescriptor(node, service, pkg);
267
+ for (const diagnostic of result.diagnostics)
268
+ this.#diagnostics.push(diagnostic);
269
+ return result.source === undefined ? undefined : JSON.stringify(result.source);
270
+ }
271
+ /**
272
+ * `loadGrpcService<S>(service, package)` -> a frozen descriptor plus generated
273
+ * request/response validators and codecs for every method.
274
+ */
275
+ emitGrpcService(node, service, pkg) {
276
+ const descriptor = this.emitGrpcDescriptor(node, service, pkg);
277
+ if (descriptor === undefined)
278
+ return undefined;
279
+ const methods = [];
280
+ for (const method of node.methods) {
281
+ const validateRequest = this.emitAssert(method.request, 'value', false);
282
+ const serializeRequest = this.emitProtoEncode(method.request, method.requestName, 'value');
283
+ const deserializeRequest = this.emitProtoDecode(method.request, method.requestName, 'bytes');
284
+ const validateResponse = this.emitAssert(method.response, 'value', false);
285
+ const serializeResponse = this.emitProtoEncode(method.response, method.responseName, 'value');
286
+ const deserializeResponse = this.emitProtoDecode(method.response, method.responseName, 'bytes');
287
+ if (validateRequest === undefined ||
288
+ serializeRequest === undefined ||
289
+ deserializeRequest === undefined ||
290
+ validateResponse === undefined ||
291
+ serializeResponse === undefined ||
292
+ deserializeResponse === undefined) {
293
+ return undefined;
294
+ }
295
+ methods.push(`${JSON.stringify(method.name)}: { ` +
296
+ `path: ${JSON.stringify(`/${pkg}.${service}/${method.name}`)}, ` +
297
+ `requestStream: ${String(method.requestStream)}, ` +
298
+ `responseStream: ${String(method.responseStream)}, ` +
299
+ `validateRequest: (value) => ${validateRequest}, ` +
300
+ `serializeRequest: (value) => ${serializeRequest}, ` +
301
+ `deserializeRequest: (bytes) => ${deserializeRequest}, ` +
302
+ `validateResponse: (value) => ${validateResponse}, ` +
303
+ `serializeResponse: (value) => ${serializeResponse}, ` +
304
+ `deserializeResponse: (bytes) => ${deserializeResponse} }`);
305
+ }
306
+ return `${this.#freeze()}({ name: ${JSON.stringify(`${pkg}.${service}`)}, descriptor: ${descriptor}, methods: { ${methods.join(', ')} } })`;
307
+ }
308
+ /** `protoEncode<T>(expr)` -> a call to a compile-time-produced straight-line encoder. */
309
+ emitProtoEncode(node, name, expr) {
310
+ const fingerprint = `protoEncode:${name}:${JSON.stringify(node)}`;
311
+ const cached = this.#shared.get(fingerprint);
312
+ if (cached !== undefined)
313
+ return `${cached}(${expr.trim()})`;
314
+ const result = emitProtoEncoder(node, name, {
315
+ namespace: this.#name('Proto'),
316
+ writer: `${this.#prefix}ProtoWriter`,
317
+ });
318
+ for (const diagnostic of result.diagnostics)
319
+ this.#diagnostics.push(diagnostic);
320
+ if (result.entry === undefined)
321
+ return undefined;
322
+ if (!this.#append(result.helpers))
323
+ return undefined;
324
+ this.#needsProtoWriter = true;
325
+ this.#shared.set(fingerprint, result.entry);
326
+ return `${result.entry}(${expr.trim()})`;
327
+ }
328
+ /** `protoDecode<T>(expr)` -> a field-number-dispatched decoder over a bounded reader. */
329
+ emitProtoDecode(node, name, expr) {
330
+ const fingerprint = `protoDecode:${name}:${JSON.stringify(node)}`;
331
+ const cached = this.#shared.get(fingerprint);
332
+ if (cached !== undefined)
333
+ return `${cached}(new ${this.#prefix}ProtoReader(${expr.trim()}))`;
334
+ const result = emitProtoDecoder(node, name, {
335
+ namespace: this.#name('Proto'),
336
+ reader: `${this.#prefix}ProtoReader`,
337
+ });
338
+ for (const diagnostic of result.diagnostics)
339
+ this.#diagnostics.push(diagnostic);
340
+ if (result.entry === undefined)
341
+ return undefined;
342
+ if (!this.#append(result.helpers))
343
+ return undefined;
344
+ this.#needsProtoReader = true;
345
+ this.#shared.set(fingerprint, result.entry);
346
+ return `${result.entry}(new ${this.#prefix}ProtoReader(${expr.trim()}))`;
347
+ }
348
+ /**
349
+ * A finished value, hoisted once and deeply frozen.
350
+ *
351
+ * Both callers compute their answer at build time rather than emitting code that
352
+ * computes it later, so what is left is printing — and the IR is JSON by construction
353
+ * (`ir/index.ts`'s first constraint), which makes `JSON.stringify` the printer.
354
+ *
355
+ * Shared by fingerprint, so ten routes asking for the same document, or four modules
356
+ * asking for the same schema, carry one copy. Which is why it is frozen: the value
357
+ * path hands back a fresh object per call, and a shared literal one consumer could
358
+ * mutate would be visible to the other nine. Frozen, that mistake is a `TypeError` at
359
+ * the assignment instead.
360
+ */
361
+ #literal(kind, label, value) {
362
+ const printed = JSON.stringify(value);
363
+ const fingerprint = `${kind}:${printed}`;
364
+ const cached = this.#shared.get(fingerprint);
365
+ if (cached !== undefined)
366
+ return cached;
367
+ const slot = this.#reserve();
368
+ if (slot === undefined)
369
+ return undefined;
370
+ const name = this.#name(label);
371
+ this.#helpers[slot] = `const ${name} = ${this.#freeze()}(${printed});`;
372
+ this.#shared.set(fingerprint, name);
373
+ return name;
374
+ }
375
+ /**
376
+ * The name of a hoisted function implementing `target` for `node`, for callers that
377
+ * want the function rather than an inlined call site — the differential suite and the
378
+ * emitted-output snapshots both do.
379
+ */
380
+ helper(node, target) {
381
+ this.#open.clear();
382
+ this.#definitions.clear();
383
+ switch (target) {
384
+ case 'check': {
385
+ const check = this.#check(node, '_v', '');
386
+ return check === undefined ? undefined : this.#function('Check', ['_v'], [`return ${check};`]);
387
+ }
388
+ case 'excess': {
389
+ const guards = this.#excess(node, '_v', '');
390
+ return guards === undefined ? undefined : this.#function('Excess', ['_v'], [...guards, 'return true;']);
391
+ }
392
+ case 'issues':
393
+ return this.#issuesHelper(node);
394
+ case 'sample': {
395
+ const sample = this.#sample(node, '');
396
+ return sample === undefined ? undefined : this.#function('Sample', [], [`return ${sample};`]);
397
+ }
398
+ }
399
+ }
400
+ // -------------------------------------------------------------------------
401
+ // Plumbing
402
+ // -------------------------------------------------------------------------
403
+ /**
404
+ * Populate the named-object table and collapse an ineffective finite cap onto the
405
+ * existing full-depth path. That keeps `is<T>` and `isShallow<T, 99>` sharing the
406
+ * same helper when `T` has no constructor below depth 99.
407
+ */
408
+ #depthFor(node, maxDepth) {
409
+ this.#definitions.clear();
410
+ this.#collectDefinitions(node, new Set());
411
+ if (maxDepth === undefined)
412
+ return undefined;
413
+ const required = Math.max(1, this.#requiredDepth(node, new Set()));
414
+ return maxDepth >= required ? undefined : maxDepth;
415
+ }
416
+ #collectDefinitions(node, seen) {
417
+ if (seen.has(node))
418
+ return;
419
+ seen.add(node);
420
+ switch (node.kind) {
421
+ case 'object':
422
+ if (node.name !== undefined)
423
+ this.#definitions.set(node.name, node);
424
+ for (const property of node.properties)
425
+ this.#collectDefinitions(property.type, seen);
426
+ return;
427
+ case 'array':
428
+ this.#collectDefinitions(node.element, seen);
429
+ return;
430
+ case 'tuple':
431
+ for (const element of node.elements)
432
+ this.#collectDefinitions(element, seen);
433
+ return;
434
+ case 'union':
435
+ for (const member of node.members)
436
+ this.#collectDefinitions(member, seen);
437
+ return;
438
+ default:
439
+ return;
440
+ }
441
+ }
442
+ /** Smallest finite `D` that emits the same walk as full depth; recursion is infinite. */
443
+ #requiredDepth(node, visiting) {
444
+ switch (node.kind) {
445
+ case 'object': {
446
+ if (node.name !== undefined && visiting.has(node.name))
447
+ return Number.POSITIVE_INFINITY;
448
+ const nested = new Set(visiting);
449
+ if (node.name !== undefined)
450
+ nested.add(node.name);
451
+ let required = 1;
452
+ for (const property of node.properties) {
453
+ required = Math.max(required, 1 + this.#requiredDepth(property.type, nested));
454
+ }
455
+ return required;
456
+ }
457
+ case 'array':
458
+ return 1 + Math.max(1, this.#requiredDepth(node.element, visiting));
459
+ case 'tuple': {
460
+ if (node.elements.length === 0)
461
+ return 1;
462
+ let required = 0;
463
+ for (const element of node.elements)
464
+ required = Math.max(required, this.#requiredDepth(element, visiting));
465
+ return 1 + Math.max(1, required);
466
+ }
467
+ case 'union': {
468
+ let required = 0;
469
+ for (const member of node.members)
470
+ required = Math.max(required, this.#requiredDepth(member, visiting));
471
+ return required;
472
+ }
473
+ case 'ref': {
474
+ const target = this.#definitions.get(node.name);
475
+ return target === undefined ? Number.POSITIVE_INFINITY : this.#requiredDepth(target, visiting);
476
+ }
477
+ default:
478
+ return 0;
479
+ }
480
+ }
481
+ #refuse(path, reason, source) {
482
+ this.#diagnostics.push(source === undefined ? { path, reason } : { path, reason, source });
483
+ return undefined;
484
+ }
485
+ #name(hint) {
486
+ return `${this.#prefix}${hint}${this.#counter++}`;
487
+ }
488
+ /** Reserve a slot so a recursive walk can name a helper before it exists. */
489
+ #reserve() {
490
+ if (this.#helpers.length >= this.#maxHelpers) {
491
+ return this.#refuse('', `more than ${this.#maxHelpers} emitted helpers in one file`);
492
+ }
493
+ this.#helpers.push(undefined);
494
+ return this.#helpers.length - 1;
495
+ }
496
+ #function(hint, parameters, body) {
497
+ const slot = this.#reserve();
498
+ if (slot === undefined)
499
+ return undefined;
500
+ const name = this.#name(hint);
501
+ this.#helpers[slot] = `function ${name}(${parameters.join(', ')}) { ${body.join(' ')} }`;
502
+ return name;
503
+ }
504
+ /** Append a complete external back-end's helpers without weakening the per-file cap. */
505
+ #append(helpers) {
506
+ if (this.#helpers.length + helpers.length > this.#maxHelpers) {
507
+ this.#refuse('', `more than ${this.#maxHelpers} emitted helpers in one file`);
508
+ return false;
509
+ }
510
+ this.#helpers.push(...helpers);
511
+ return true;
512
+ }
513
+ #bind(expr, captureProperty = false) {
514
+ const trimmed = expr.trim();
515
+ if (SIMPLE_REFERENCE.test(trimmed) && (!captureProperty || IDENTIFIER.test(trimmed))) {
516
+ // Keep simple references inline unless their checks require one captured
517
+ // property value, as bounded numeric predicates do.
518
+ return {
519
+ ref: trimmed,
520
+ expression: body => body,
521
+ block: statements => `((() => { ${statements.join(' ')} })())`,
522
+ };
523
+ }
524
+ // Anything with a call, an index or an operator in it is evaluated exactly once:
525
+ // `assert<T>(next())` must not advance the iterator twice.
526
+ const parameter = this.#name('Arg');
527
+ return {
528
+ ref: parameter,
529
+ expression: body => `((${parameter}) => ${body})(${trimmed})`,
530
+ block: statements => `((${parameter}) => { ${statements.join(' ')} })(${trimmed})`,
531
+ };
532
+ }
533
+ /**
534
+ * The shared shape of `assert` and `validate`: an allocation-free gate, and the issues
535
+ * walker to run only once the gate has said no.
536
+ */
537
+ #twoPass(node, expr, strict, maxDepth) {
538
+ this.#open.clear();
539
+ const depth = this.#depthFor(node, maxDepth);
540
+ const bound = this.#bind(expr);
541
+ const check = this.#check(node, bound.ref, '', depth);
542
+ if (check === undefined)
543
+ return undefined;
544
+ const excess = strict ? this.#excessHelper(node) : null;
545
+ if (excess === undefined)
546
+ return undefined;
547
+ const issues = this.#issuesHelper(node, depth);
548
+ if (issues === undefined)
549
+ return undefined;
550
+ const gate = excess === null ? `(${check})` : `((${check}) && ${excess}(${bound.ref}))`;
551
+ if (excess === null)
552
+ return { bound, gate, collect: issues };
553
+ // Excess properties are one issue about the value as a whole, and only worth
554
+ // reporting when nothing else was wrong: "you also passed `extra`" is noise next to
555
+ // "`email` is not a string".
556
+ const collect = this.#function('Strict', ['_v', '_p', '_o'], [
557
+ `${issues}(_v, _p, _o);`,
558
+ `if (_o.length === 0 && !${excess}(_v)) ${this.#issue('_o', '_p', JSON.stringify('no excess properties'), '_v')}`,
559
+ ]);
560
+ return collect === undefined ? undefined : { bound, gate, collect };
561
+ }
562
+ /** A hoisted `(v) => boolean` excess check, or `null` when the type has none. */
563
+ #excessHelper(node) {
564
+ const fingerprint = `excessFn:${JSON.stringify(node)}`;
565
+ const cached = this.#shared.get(fingerprint);
566
+ if (cached !== undefined)
567
+ return cached;
568
+ const guards = this.#excess(node, '_v', '');
569
+ if (guards === undefined)
570
+ return undefined;
571
+ if (guards.length === 0)
572
+ return null;
573
+ // A named type's excess walk is already a hoisted function, and wrapping
574
+ // `if (!_zmdbExcessUser1(_v)) return false; return true;` in a second one buys a call
575
+ // per validation and nothing else. Hand back the inner function instead.
576
+ const [only] = guards;
577
+ const inner = guards.length === 1 && only !== undefined ? SOLE_GUARD.exec(only)?.[1] : undefined;
578
+ if (inner !== undefined) {
579
+ this.#shared.set(fingerprint, inner);
580
+ return inner;
581
+ }
582
+ const name = this.#function('Excess', ['_v'], [...guards, 'return true;']);
583
+ if (name === undefined)
584
+ return undefined;
585
+ this.#shared.set(fingerprint, name);
586
+ return name;
587
+ }
588
+ #issuesHelper(node, depth) {
589
+ const fingerprint = `issuesFn${depthKey(depth)}:${JSON.stringify(node)}`;
590
+ const cached = this.#shared.get(fingerprint);
591
+ if (cached !== undefined)
592
+ return cached;
593
+ const slot = this.#reserve();
594
+ if (slot === undefined)
595
+ return undefined;
596
+ const name = this.#name('Issues');
597
+ this.#shared.set(fingerprint, name);
598
+ const statements = this.#issues(node, '_v', '_p', '_o', '', depth);
599
+ if (statements === undefined) {
600
+ this.#shared.delete(fingerprint);
601
+ return undefined;
602
+ }
603
+ this.#helpers[slot] = `function ${name}(_v, _p, _o) { ${statements.join(' ')} }`;
604
+ return name;
605
+ }
606
+ /**
607
+ * The deep-freeze used by `emitJsonSchema`, hoisted once per file.
608
+ *
609
+ * A helper rather than nested `Object.freeze(…)` calls inside the literal: the point of
610
+ * emitting a document is that the emitted source reads as the document, and wrapping
611
+ * every nested object in a call buries it. A recursive walk over a schema document at
612
+ * module load is not a cost worth optimising.
613
+ */
614
+ #freeze() {
615
+ const name = `${this.#prefix}Freeze`;
616
+ if (!this.#hasFreeze) {
617
+ this.#hasFreeze = true;
618
+ this.#helpers.push(`function ${name}(_v) { if (_v !== null && typeof _v === "object") { for (const _k of Object.keys(_v)) ${name}(_v[_k]); Object.freeze(_v); } return _v; }`);
619
+ }
620
+ return name;
621
+ }
622
+ /** The one-liner that records a failure. Hoisted once per file. */
623
+ #issue(out, path, expected, value) {
624
+ if (!this.#hasIssueHelper) {
625
+ this.#hasIssueHelper = true;
626
+ this.#helpers.push(`function ${this.#prefix}Issue(out, path, expected, value) { out.push({ path, expected, value, message: "expected " + expected }); }`);
627
+ }
628
+ return `${this.#prefix}Issue(${out}, ${path}, ${expected}, ${value});`;
629
+ }
630
+ // -------------------------------------------------------------------------
631
+ // Target: check
632
+ // -------------------------------------------------------------------------
633
+ #check(node, v, path, depth) {
634
+ switch (node.kind) {
635
+ case 'unsupported':
636
+ return this.#refuse(path, node.reason, node.source);
637
+ case 'unknown':
638
+ return 'true';
639
+ case 'null':
640
+ return `${v} === null`;
641
+ case 'undefined':
642
+ return `${v} === undefined`;
643
+ case 'literal':
644
+ return `${v} === ${JSON.stringify(node.value)}`;
645
+ case 'scalar':
646
+ return this.#scalarCheck(node, v, path);
647
+ case 'union':
648
+ return this.#unionCheck(node, v, path, depth);
649
+ case 'tuple':
650
+ return this.#tupleCheck(node, v, path, depth);
651
+ case 'array':
652
+ return this.#arrayCheck(node, v, path, depth);
653
+ case 'object':
654
+ return this.#objectCheck(node, v, path, depth);
655
+ case 'ref': {
656
+ if (depth !== undefined) {
657
+ const target = this.#definitions.get(node.name);
658
+ if (target === undefined) {
659
+ return this.#refuse(path, `a back-reference to \`${node.name}\`, which was never declared`);
660
+ }
661
+ return this.#objectCheck(target, v, path, depth);
662
+ }
663
+ const helper = this.#open.get(`check:${node.name}`);
664
+ if (helper === undefined) {
665
+ return this.#refuse(path, `a back-reference to \`${node.name}\`, which was never declared`);
666
+ }
667
+ return `${helper}(${v})`;
668
+ }
669
+ }
670
+ }
671
+ #scalarCheck(node, v, path) {
672
+ const boundedNumber = node.scalar === 'number' && (node.constraints?.minimum !== undefined || node.constraints?.maximum !== undefined);
673
+ const bound = boundedNumber ? this.#bind(v, true) : undefined;
674
+ const value = bound?.ref ?? v;
675
+ const constraints = this.#constraintChecks(node.constraints, value, node.scalar === 'string', path);
676
+ if (constraints === undefined)
677
+ return undefined;
678
+ // A numeric comparison rejects NaN itself, provided every check reads the
679
+ // same captured value. Unbounded numbers still need their explicit guard.
680
+ const base = boundedNumber ? `typeof ${value} === "number"` : scalarBase(node.scalar, value);
681
+ const check = [base, ...constraints].join(' && ');
682
+ return bound?.expression(check) ?? check;
683
+ }
684
+ /** `length`-based bounds read a `.length`; numeric ones compare the value itself. */
685
+ #constraintChecks(constraints, v, lengthy, path) {
686
+ const parts = [];
687
+ if (!constraints)
688
+ return parts;
689
+ if (constraints.minimum !== undefined)
690
+ parts.push(`${v} >= ${constraints.minimum}`);
691
+ if (constraints.maximum !== undefined)
692
+ parts.push(`${v} <= ${constraints.maximum}`);
693
+ if (constraints.minLength !== undefined)
694
+ parts.push(`${v}.length >= ${constraints.minLength}`);
695
+ if (constraints.maxLength !== undefined)
696
+ parts.push(`${v}.length <= ${constraints.maxLength}`);
697
+ if (constraints.pattern !== undefined) {
698
+ if (!lengthy)
699
+ return this.#refuse(path, 'a `pattern` constraint on something that is not a string');
700
+ // Validated here rather than trusted: an unparseable pattern would otherwise
701
+ // become a syntax error in the emitted module, which is a far worse message.
702
+ validatePatternComplexity(constraints.pattern);
703
+ parts.push(`/${escapePattern(constraints.pattern)}/.test(${v})`);
704
+ }
705
+ return parts;
706
+ }
707
+ #unionCheck(node, v, path, depth) {
708
+ if (node.members.length === 0)
709
+ return this.#refuse(path, 'an empty union matches nothing');
710
+ if (node.members.length > MANY_LITERALS && node.members.every(member => member.kind === 'literal')) {
711
+ // A long enum is a set lookup, not a chain of `===`. Below the cutoff the chain is
712
+ // faster and reads better; above it, the `Set` wins.
713
+ //
714
+ // boundary: `every` above proved each member is a `literal`, but it returns a boolean
715
+ // and the narrowing does not reach this `map`. The alternative is testing `kind` again
716
+ // inside the map for a branch the condition has ruled out.
717
+ const values = node.members.map(member => JSON.stringify(member.value));
718
+ const name = this.#name('Set');
719
+ this.#helpers.push(`const ${name} = new Set([${values.join(', ')}]);`);
720
+ return `${name}.has(${v})`;
721
+ }
722
+ const discriminant = discriminantOf(node.members);
723
+ if (discriminant) {
724
+ const arms = [];
725
+ for (const arm of discriminant.arms) {
726
+ const body = this.#objectBody(arm.node, v, path, { skip: discriminant.key, bare: true }, depth);
727
+ if (body === undefined)
728
+ return undefined;
729
+ arms.push(`${v}${accessor(discriminant.key)} === ${JSON.stringify(arm.value)} ? (${body})`);
730
+ }
731
+ return `(${recordTest(v)} && (${arms.join(' : ')} : false))`;
732
+ }
733
+ const parts = [];
734
+ for (const [index, member] of node.members.entries()) {
735
+ const check = this.#check(member, v, `${path}|${index}`, depth);
736
+ if (check === undefined)
737
+ return undefined;
738
+ parts.push(`(${check})`);
739
+ }
740
+ return `(${parts.join(' || ')})`;
741
+ }
742
+ #tupleCheck(node, v, path, depth) {
743
+ const parts = [`Array.isArray(${v})`, `${v}.length === ${node.elements.length}`];
744
+ if (depth !== undefined && depth <= 1)
745
+ return parts.join(' && ');
746
+ const nestedDepth = childDepth(depth);
747
+ for (const [index, element] of node.elements.entries()) {
748
+ const check = this.#check(element, `${v}[${index}]`, `${path}[${index}]`, nestedDepth);
749
+ if (check === undefined)
750
+ return undefined;
751
+ if (check !== 'true')
752
+ parts.push(`(${check})`);
753
+ }
754
+ return parts.join(' && ');
755
+ }
756
+ #arrayCheck(node, v, path, depth) {
757
+ if (depth !== undefined && depth <= 1) {
758
+ const bounds = this.#constraintChecks(node.constraints, v, true, path);
759
+ if (bounds === undefined)
760
+ return undefined;
761
+ return [`Array.isArray(${v})`, ...bounds].join(' && ');
762
+ }
763
+ const fingerprint = `checkArray${depthKey(depth)}:${JSON.stringify(node)}`;
764
+ const cached = this.#shared.get(fingerprint);
765
+ if (cached !== undefined)
766
+ return `${cached}(${v})`;
767
+ const slot = this.#reserve();
768
+ if (slot === undefined)
769
+ return undefined;
770
+ const name = this.#name('CheckArray');
771
+ this.#shared.set(fingerprint, name);
772
+ const element = this.#check(node.element, '_v[_i]', `${path}[]`, childDepth(depth));
773
+ if (element === undefined) {
774
+ this.#shared.delete(fingerprint);
775
+ return undefined;
776
+ }
777
+ const helperBounds = this.#constraintChecks(node.constraints, '_v', true, path);
778
+ if (helperBounds === undefined)
779
+ return undefined;
780
+ const body = [`if (!Array.isArray(_v)) return false;`];
781
+ for (const bound of helperBounds)
782
+ body.push(`if (!(${bound})) return false;`);
783
+ if (element !== 'true') {
784
+ body.push(`for (let _i = 0; _i < _v.length; _i++) { if (!(${element})) return false; }`);
785
+ }
786
+ body.push('return true;');
787
+ this.#helpers[slot] = `function ${name}(_v) { ${body.join(' ')} }`;
788
+ return `${name}(${v})`;
789
+ }
790
+ #objectCheck(node, v, path, depth) {
791
+ if (node.name !== undefined)
792
+ this.#definitions.set(node.name, node);
793
+ if (depth === 0)
794
+ return recordTest(v);
795
+ if (node.name === undefined)
796
+ return this.#objectBody(node, v, path, {}, depth);
797
+ const openKey = `check${depthKey(depth)}:${node.name}`;
798
+ const open = this.#open.get(openKey);
799
+ if (open !== undefined)
800
+ return `${open}(${v})`;
801
+ const fingerprint = `check${depthKey(depth)}:${JSON.stringify(node)}`;
802
+ const cached = this.#shared.get(fingerprint);
803
+ if (cached !== undefined)
804
+ return `${cached}(${v})`;
805
+ const slot = this.#reserve();
806
+ if (slot === undefined)
807
+ return undefined;
808
+ const name = this.#name(`Check${capitalise(node.name)}`);
809
+ this.#open.set(openKey, name);
810
+ this.#shared.set(fingerprint, name);
811
+ const body = this.#objectBody(node, '_v', path, {}, depth);
812
+ if (body === undefined)
813
+ return undefined;
814
+ this.#helpers[slot] = `function ${name}(_v) { return ${body}; }`;
815
+ return `${name}(${v})`;
816
+ }
817
+ #objectBody(node, v, path, options, depth) {
818
+ if (node.name !== undefined)
819
+ this.#definitions.set(node.name, node);
820
+ const parts = options.bare === true ? [] : [recordTest(v)];
821
+ if (depth === 0)
822
+ return parts.length === 0 ? 'true' : parts.join(' && ');
823
+ const nestedDepth = childDepth(depth);
824
+ for (const property of node.properties) {
825
+ if (property.name === options.skip)
826
+ continue;
827
+ const member = `${v}${accessor(property.name)}`;
828
+ const check = this.#check(property.type, member, join(path, property.name), nestedDepth);
829
+ if (check === undefined)
830
+ return undefined;
831
+ if (check === 'true')
832
+ continue;
833
+ parts.push(property.optional ? `(${member} === undefined || (${check}))` : `(${check})`);
834
+ }
835
+ return parts.length === 0 ? 'true' : parts.join(' && ');
836
+ }
837
+ // -------------------------------------------------------------------------
838
+ // Target: excess
839
+ // -------------------------------------------------------------------------
840
+ /**
841
+ * Statements that `return false` when the value carries a property the type does not
842
+ * declare. Reached only after `check` has passed, so every declared property is known
843
+ * to be there — which is what lets an all-required object reduce "no excess keys" to a
844
+ * key count.
845
+ */
846
+ #excess(node, v, path) {
847
+ switch (node.kind) {
848
+ case 'object':
849
+ return this.#objectExcess(node, v, path);
850
+ case 'array':
851
+ return this.#arrayExcess(node, v, path);
852
+ case 'tuple': {
853
+ const statements = [];
854
+ for (const [index, element] of node.elements.entries()) {
855
+ if (!hasExcessCheck(element))
856
+ continue;
857
+ const inner = this.#excess(element, `${v}[${index}]`, `${path}[${index}]`);
858
+ if (inner === undefined)
859
+ return undefined;
860
+ statements.push(...inner);
861
+ }
862
+ return statements;
863
+ }
864
+ case 'union': {
865
+ // A value can satisfy several arms of an undiscriminated union, so "which arm's
866
+ // property list is the declared one" has no answer and neither path checks it.
867
+ // With a discriminant there is exactly one answer.
868
+ const discriminant = discriminantOf(node.members);
869
+ if (!discriminant)
870
+ return [];
871
+ const branches = [];
872
+ for (const arm of discriminant.arms) {
873
+ const inner = this.#excess(arm.node, v, path);
874
+ if (inner === undefined)
875
+ return undefined;
876
+ if (inner.length === 0)
877
+ continue;
878
+ branches.push(`if (${v}${accessor(discriminant.key)} === ${JSON.stringify(arm.value)}) { ${inner.join(' ')} }`);
879
+ }
880
+ return branches;
881
+ }
882
+ case 'ref': {
883
+ // Every named object hoists, and registers itself before its own body is
884
+ // walked, so an ancestor a `ref` points at always has a helper by the time the
885
+ // `ref` is reached. The guard is here because "always" is a claim about the
886
+ // reflector, and emitting nothing is safer than emitting a call to nothing.
887
+ const helper = this.#open.get(`excess:${node.name}`);
888
+ if (helper === undefined) {
889
+ return this.#refuse(path, `a back-reference to \`${node.name}\`, which was never declared`);
890
+ }
891
+ return [`if (!${helper}(${v})) return false;`];
892
+ }
893
+ default:
894
+ return [];
895
+ }
896
+ }
897
+ #objectExcess(node, v, path) {
898
+ // A named object always hoists, even at the top. Inlining it there would leave
899
+ // `excess:<name>` unregistered, so a `ref` back to it inside its own body would
900
+ // find no helper and silently skip the check the runtime walker still performs.
901
+ if (node.name === undefined)
902
+ return this.#objectExcessBody(node, v, path);
903
+ const openKey = `excess:${node.name}`;
904
+ const open = this.#open.get(openKey);
905
+ if (open !== undefined)
906
+ return [`if (!${open}(${v})) return false;`];
907
+ const fingerprint = `excess:${JSON.stringify(node)}`;
908
+ const cached = this.#shared.get(fingerprint);
909
+ if (cached !== undefined)
910
+ return [`if (!${cached}(${v})) return false;`];
911
+ const slot = this.#reserve();
912
+ if (slot === undefined)
913
+ return undefined;
914
+ const name = this.#name(`Excess${capitalise(node.name)}`);
915
+ this.#open.set(openKey, name);
916
+ this.#shared.set(fingerprint, name);
917
+ const inner = this.#objectExcessBody(node, '_v', path);
918
+ if (inner === undefined)
919
+ return undefined;
920
+ this.#helpers[slot] = `function ${name}(_v) { ${inner.join(' ')} return true; }`;
921
+ return [`if (!${name}(${v})) return false;`];
922
+ }
923
+ #objectExcessBody(node, v, path) {
924
+ const statements = [];
925
+ const names = node.properties.map(property => property.name);
926
+ if (names.length > 0 && node.properties.every(property => !property.optional)) {
927
+ // Fast path: nothing is optional, so every declared key is present and "no excess"
928
+ // is exactly "the counts agree". No Set, no allocation, and the loop bails as soon
929
+ // as it has seen one key too many.
930
+ const counter = this.#name('C');
931
+ statements.push(`let ${counter} = 0; for (const _ in ${v}) { if (++${counter} > ${names.length}) return false; } if (${counter} !== ${names.length}) return false;`);
932
+ }
933
+ else {
934
+ const set = this.#name('Keys');
935
+ this.#helpers.push(`const ${set} = new Set([${names.map(name => JSON.stringify(name)).join(', ')}]);`);
936
+ statements.push(`for (const _k in ${v}) { if (!${set}.has(_k)) return false; }`);
937
+ }
938
+ for (const property of node.properties) {
939
+ if (!hasExcessCheck(property.type))
940
+ continue;
941
+ const member = `${v}${accessor(property.name)}`;
942
+ const inner = this.#excess(property.type, member, join(path, property.name));
943
+ if (inner === undefined)
944
+ return undefined;
945
+ if (inner.length === 0)
946
+ continue;
947
+ // The nested value may legitimately be absent (an optional property) or not an
948
+ // object at all (a `T | null`), and `for (const _ in undefined)` throws.
949
+ statements.push(`if (${keyableTest(member)}) { ${inner.join(' ')} }`);
950
+ }
951
+ return statements;
952
+ }
953
+ #arrayExcess(node, v, path) {
954
+ if (!hasExcessCheck(node.element))
955
+ return [];
956
+ const index = this.#name('I');
957
+ const inner = this.#excess(node.element, `${v}[${index}]`, `${path}[]`);
958
+ if (inner === undefined)
959
+ return undefined;
960
+ if (inner.length === 0)
961
+ return [];
962
+ return [`for (let ${index} = 0; ${index} < ${v}.length; ${index}++) { ${inner.join(' ')} }`];
963
+ }
964
+ // -------------------------------------------------------------------------
965
+ // Target: issues
966
+ // -------------------------------------------------------------------------
967
+ #issues(node, v, p, out, path, depth) {
968
+ switch (node.kind) {
969
+ case 'unsupported':
970
+ return this.#refuse(path, node.reason, node.source);
971
+ case 'unknown':
972
+ return [];
973
+ case 'null':
974
+ case 'undefined':
975
+ case 'literal': {
976
+ const check = this.#check(node, v, path, depth);
977
+ if (check === undefined)
978
+ return undefined;
979
+ return [`if (!(${check})) ${this.#issue(out, p, JSON.stringify(expectedOf(node)), v)}`];
980
+ }
981
+ case 'scalar':
982
+ return this.#scalarIssues(node, v, p, out, path);
983
+ case 'tuple':
984
+ return this.#tupleIssues(node, v, p, out, path, depth);
985
+ case 'array':
986
+ return this.#arrayIssues(node, v, p, out, path, depth);
987
+ case 'object':
988
+ return this.#objectIssues(node, v, p, out, path, depth);
989
+ case 'union':
990
+ return this.#unionIssues(node, v, p, out, path, depth);
991
+ case 'ref': {
992
+ if (depth !== undefined) {
993
+ const target = this.#definitions.get(node.name);
994
+ if (target === undefined) {
995
+ return this.#refuse(path, `a back-reference to \`${node.name}\`, which was never declared`);
996
+ }
997
+ return this.#objectIssues(target, v, p, out, path, depth);
998
+ }
999
+ const helper = this.#open.get(`issues:${node.name}`);
1000
+ if (helper === undefined) {
1001
+ return this.#refuse(path, `a back-reference to \`${node.name}\`, which was never declared`);
1002
+ }
1003
+ return [`${helper}(${v}, ${p}, ${out});`];
1004
+ }
1005
+ }
1006
+ }
1007
+ #scalarIssues(node, v, p, out, path) {
1008
+ const base = scalarBase(node.scalar, v);
1009
+ const bounds = this.#constraintIssues(node.constraints, v, p, out, node.scalar === 'string', path);
1010
+ if (bounds === undefined)
1011
+ return undefined;
1012
+ const shape = this.#issue(out, p, JSON.stringify(expectedOf(node)), v);
1013
+ // The shape is checked first and stops the walk: reporting `minLength 3` about a
1014
+ // number would be two issues where one is the truth.
1015
+ if (bounds.length === 0)
1016
+ return [`if (!(${base})) ${shape}`];
1017
+ return [`if (!(${base})) { ${shape} } else { ${bounds.join(' ')} }`];
1018
+ }
1019
+ #constraintIssues(constraints, v, p, out, lengthy, path) {
1020
+ if (!constraints)
1021
+ return [];
1022
+ const statements = [];
1023
+ const push = (keyword, test, value) => {
1024
+ statements.push(`if (!(${test})) ${this.#issue(out, p, JSON.stringify(expectedForConstraint(keyword, value)), v)}`);
1025
+ };
1026
+ if (constraints.minimum !== undefined)
1027
+ push('minimum', `${v} >= ${constraints.minimum}`, constraints.minimum);
1028
+ if (constraints.maximum !== undefined)
1029
+ push('maximum', `${v} <= ${constraints.maximum}`, constraints.maximum);
1030
+ if (constraints.minLength !== undefined) {
1031
+ push('minLength', `${v}.length >= ${constraints.minLength}`, constraints.minLength);
1032
+ }
1033
+ if (constraints.maxLength !== undefined) {
1034
+ push('maxLength', `${v}.length <= ${constraints.maxLength}`, constraints.maxLength);
1035
+ }
1036
+ if (constraints.pattern !== undefined) {
1037
+ if (!lengthy)
1038
+ return this.#refuse(path, 'a `pattern` constraint on something that is not a string');
1039
+ validatePatternComplexity(constraints.pattern);
1040
+ push('pattern', `/${escapePattern(constraints.pattern)}/.test(${v})`, constraints.pattern);
1041
+ }
1042
+ return statements;
1043
+ }
1044
+ #tupleIssues(node, v, p, out, path, depth) {
1045
+ const inner = [];
1046
+ if (depth === undefined || depth > 1) {
1047
+ const nestedDepth = childDepth(depth);
1048
+ for (const [index, element] of node.elements.entries()) {
1049
+ const statements = this.#issues(element, `${v}[${index}]`, indexed(p, String(index)), out, `${path}[${index}]`, nestedDepth);
1050
+ if (statements === undefined)
1051
+ return undefined;
1052
+ inner.push(...statements);
1053
+ }
1054
+ }
1055
+ const shape = this.#issue(out, p, JSON.stringify(expectedOf(node)), v);
1056
+ return [
1057
+ `if (!Array.isArray(${v}) || ${v}.length !== ${node.elements.length}) { ${shape} } else { ${inner.join(' ')} }`,
1058
+ ];
1059
+ }
1060
+ #arrayIssues(node, v, p, out, path, depth) {
1061
+ const fingerprint = `issuesArray${depthKey(depth)}:${JSON.stringify(node)}`;
1062
+ const cached = this.#shared.get(fingerprint);
1063
+ if (cached !== undefined)
1064
+ return [`${cached}(${v}, ${p}, ${out});`];
1065
+ const slot = this.#reserve();
1066
+ if (slot === undefined)
1067
+ return undefined;
1068
+ const name = this.#name('IssuesArray');
1069
+ this.#shared.set(fingerprint, name);
1070
+ let element = [];
1071
+ if (depth === undefined || depth > 1) {
1072
+ const emitted = this.#issues(node.element, '_v[_i]', indexed('_p', '_i'), '_o', `${path}[]`, childDepth(depth));
1073
+ if (emitted === undefined) {
1074
+ this.#shared.delete(fingerprint);
1075
+ return undefined;
1076
+ }
1077
+ element = emitted;
1078
+ }
1079
+ const bounds = this.#constraintIssues(node.constraints, '_v', '_p', '_o', true, path);
1080
+ if (bounds === undefined)
1081
+ return undefined;
1082
+ const body = [
1083
+ `if (!Array.isArray(_v)) { ${this.#issue('_o', '_p', JSON.stringify('array'), '_v')} return; }`,
1084
+ ...bounds,
1085
+ ];
1086
+ if (depth === undefined || depth > 1) {
1087
+ body.push(`for (let _i = 0; _i < _v.length; _i++) { ${element.join(' ')} }`);
1088
+ }
1089
+ this.#helpers[slot] = `function ${name}(_v, _p, _o) { ${body.join(' ')} }`;
1090
+ return [`${name}(${v}, ${p}, ${out});`];
1091
+ }
1092
+ #objectIssues(node, v, p, out, path, depth) {
1093
+ if (node.name !== undefined)
1094
+ this.#definitions.set(node.name, node);
1095
+ if (node.name === undefined)
1096
+ return this.#objectIssuesBody(node, v, p, out, path, depth);
1097
+ const openKey = `issues${depthKey(depth)}:${node.name}`;
1098
+ const open = this.#open.get(openKey);
1099
+ if (open !== undefined)
1100
+ return [`${open}(${v}, ${p}, ${out});`];
1101
+ const fingerprint = `issues${depthKey(depth)}:${JSON.stringify(node)}`;
1102
+ const cached = this.#shared.get(fingerprint);
1103
+ if (cached !== undefined)
1104
+ return [`${cached}(${v}, ${p}, ${out});`];
1105
+ const slot = this.#reserve();
1106
+ if (slot === undefined)
1107
+ return undefined;
1108
+ const name = this.#name(`Issues${capitalise(node.name)}`);
1109
+ this.#open.set(openKey, name);
1110
+ this.#shared.set(fingerprint, name);
1111
+ const body = this.#objectIssuesBody(node, '_v', '_p', '_o', path, depth);
1112
+ if (body === undefined)
1113
+ return undefined;
1114
+ this.#helpers[slot] = `function ${name}(_v, _p, _o) { ${body.join(' ')} }`;
1115
+ return [`${name}(${v}, ${p}, ${out});`];
1116
+ }
1117
+ #objectIssuesBody(node, v, p, out, path, depth) {
1118
+ if (node.name !== undefined)
1119
+ this.#definitions.set(node.name, node);
1120
+ const inner = [];
1121
+ if (depth !== 0) {
1122
+ const nestedDepth = childDepth(depth);
1123
+ for (const property of node.properties) {
1124
+ const member = `${v}${accessor(property.name)}`;
1125
+ const statements = this.#issues(property.type, member, join(p, property.name), out, join(path, property.name), nestedDepth);
1126
+ if (statements === undefined)
1127
+ return undefined;
1128
+ if (statements.length === 0)
1129
+ continue;
1130
+ inner.push(property.optional ? `if (${member} !== undefined) { ${statements.join(' ')} }` : statements.join(' '));
1131
+ }
1132
+ }
1133
+ const shape = this.#issue(out, p, JSON.stringify(expectedOf(node)), v);
1134
+ return [`if (!(${recordTest(v)})) { ${shape} } else { ${inner.join(' ')} }`];
1135
+ }
1136
+ #unionIssues(node, v, p, out, path, depth) {
1137
+ const discriminant = discriminantOf(node.members);
1138
+ if (!discriminant) {
1139
+ // No discriminant, so there is no arm to blame: one issue naming the whole union.
1140
+ const check = this.#check(node, v, path, depth);
1141
+ if (check === undefined)
1142
+ return undefined;
1143
+ return [`if (!(${check})) ${this.#issue(out, p, JSON.stringify(expectedOf(node)), v)}`];
1144
+ }
1145
+ const branches = [];
1146
+ for (const arm of discriminant.arms) {
1147
+ const body = this.#objectIssuesBody(arm.node, v, p, out, path, depth);
1148
+ if (body === undefined)
1149
+ return undefined;
1150
+ branches.push(`if (${v}${accessor(discriminant.key)} === ${JSON.stringify(arm.value)}) { ${body.join(' ')} }`);
1151
+ }
1152
+ const shape = this.#issue(out, p, JSON.stringify(expectedOf(node)), v);
1153
+ const wrongKey = this.#issue(out, join(p, discriminant.key), JSON.stringify(expectedForDiscriminant(discriminant)), `${v}${accessor(discriminant.key)}`);
1154
+ return [`if (!(${recordTest(v)})) { ${shape} } else ${branches.join(' else ')} else { ${wrongKey} }`];
1155
+ }
1156
+ // -------------------------------------------------------------------------
1157
+ // Target: sample
1158
+ // -------------------------------------------------------------------------
1159
+ /**
1160
+ * A value that satisfies `node` **by construction**. Where that cannot be promised the
1161
+ * emitter refuses, which is the difference between this and what it replaced: the old
1162
+ * generator answered `'x'` for an arbitrary `pattern`, so `is(random(d), d)` — the one
1163
+ * property it claimed — was false.
1164
+ */
1165
+ #sample(node, path) {
1166
+ switch (node.kind) {
1167
+ case 'unsupported':
1168
+ return this.#refuse(path, node.reason, node.source);
1169
+ case 'unknown':
1170
+ case 'null':
1171
+ return 'null';
1172
+ case 'undefined':
1173
+ return 'undefined';
1174
+ case 'literal':
1175
+ return JSON.stringify(node.value);
1176
+ case 'scalar':
1177
+ return this.#scalarSample(node, path);
1178
+ case 'tuple': {
1179
+ const elements = [];
1180
+ for (const [index, element] of node.elements.entries()) {
1181
+ const sample = this.#sample(element, `${path}[${index}]`);
1182
+ if (sample === undefined)
1183
+ return undefined;
1184
+ elements.push(sample);
1185
+ }
1186
+ return `[${elements.join(', ')}]`;
1187
+ }
1188
+ case 'array':
1189
+ return this.#arraySample(node, path);
1190
+ case 'object': {
1191
+ const entries = [];
1192
+ for (const property of node.properties) {
1193
+ const sample = this.#sample(property.type, join(path, property.name));
1194
+ if (sample === undefined)
1195
+ return undefined;
1196
+ entries.push(`${JSON.stringify(property.name)}: ${sample}`);
1197
+ }
1198
+ return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`;
1199
+ }
1200
+ case 'union':
1201
+ return this.#unionSample(node, path);
1202
+ case 'ref':
1203
+ return this.#refuse(path, `\`${node.name}\` recurs with no terminating arm, so no finite value satisfies it`);
1204
+ }
1205
+ }
1206
+ #arraySample(node, path) {
1207
+ const element = this.#sample(node.element, `${path}[]`);
1208
+ if (element === undefined)
1209
+ return undefined;
1210
+ const min = node.constraints?.minLength ?? 1;
1211
+ const max = node.constraints?.maxLength ?? Math.max(min, 3);
1212
+ if (min > max)
1213
+ return this.#refuse(path, `an array with minLength ${min} above maxLength ${max}`);
1214
+ const name = this.#function('SampleArray', [], [
1215
+ `const _n = ${this.#randInt(min, max)};`,
1216
+ `const _a = []; for (let _i = 0; _i < _n; _i++) _a.push(${element});`,
1217
+ 'return _a;',
1218
+ ]);
1219
+ return name === undefined ? undefined : `${name}()`;
1220
+ }
1221
+ #unionSample(node, path) {
1222
+ // Back-references are dropped rather than sampled: `Node_ { next: Node_ | null }`
1223
+ // has a terminating arm and the generator takes it. A `ref` with no way out is the
1224
+ // refusal in `#sample`.
1225
+ const usable = node.members.filter(member => member.kind !== 'ref');
1226
+ if (usable.length === 0) {
1227
+ return this.#refuse(path, 'a union of nothing but back-references cannot be sampled');
1228
+ }
1229
+ const options = [];
1230
+ for (const [index, member] of usable.entries()) {
1231
+ const sample = this.#sample(member, `${path}|${index}`);
1232
+ if (sample === undefined)
1233
+ return undefined;
1234
+ options.push(sample);
1235
+ }
1236
+ // boundary: `usable` is non-empty — the line above returns otherwise — and the loop
1237
+ // pushes exactly one option per member or returns, so `options` has at least one
1238
+ // element. `noUncheckedIndexedAccess` cannot follow that, and the alternatives are a
1239
+ // refusal for a case that cannot happen or a default sample that would be emitted.
1240
+ const first = options[0];
1241
+ if (options.length === 1)
1242
+ return first;
1243
+ const cases = options.map((option, index) => `case ${index}: return ${option};`);
1244
+ const name = this.#function('SampleUnion', [], [`switch (${this.#randInt(0, options.length - 1)}) { ${cases.join(' ')} }`, `return ${first};`]);
1245
+ return name === undefined ? undefined : `${name}()`;
1246
+ }
1247
+ #scalarSample(node, path) {
1248
+ const constraints = node.constraints;
1249
+ switch (node.scalar) {
1250
+ case 'boolean':
1251
+ return 'Math.random() < 0.5';
1252
+ case 'date':
1253
+ // The same instant the runtime sampler draws, and for the same reason: one documented
1254
+ // function should not mean "now" when the transformer inlined it and "an arbitrary
1255
+ // instant" when it did not. Epoch to roughly 2024.
1256
+ return 'new Date(Math.floor(Math.random() * 1700000000000))';
1257
+ case 'number':
1258
+ case 'integer':
1259
+ case 'bigint': {
1260
+ const min = constraints?.minimum ?? 0;
1261
+ const max = constraints?.maximum ?? min + 1000;
1262
+ if (min > max)
1263
+ return this.#refuse(path, `a bound with minimum ${min} above maximum ${max}`);
1264
+ const int = this.#randInt(min, max);
1265
+ return node.scalar === 'bigint' ? `BigInt(${int})` : int;
1266
+ }
1267
+ case 'string': {
1268
+ if (constraints?.pattern !== undefined) {
1269
+ return this.#refuse(path, 'a sample cannot be built from a `pattern`; nothing here inverts a regular expression', constraints.pattern);
1270
+ }
1271
+ const min = constraints?.minLength ?? 1;
1272
+ const max = constraints?.maxLength ?? Math.max(min, 12);
1273
+ if (min > max)
1274
+ return this.#refuse(path, `a string with minLength ${min} above maxLength ${max}`);
1275
+ if (!this.#hasStringSample) {
1276
+ this.#hasStringSample = true;
1277
+ this.#helpers.push(`function ${this.#prefix}Str(min, max) { let s = ""; while (s.length < Math.max(min, 1)) s += Math.random().toString(36).slice(2); return s.slice(0, max); }`);
1278
+ }
1279
+ return `${this.#prefix}Str(${min}, ${max})`;
1280
+ }
1281
+ }
1282
+ }
1283
+ #randInt(min, max) {
1284
+ if (!this.#hasIntSample) {
1285
+ this.#hasIntSample = true;
1286
+ this.#helpers.push(`function ${this.#prefix}Int(min, max) { return min + Math.floor(Math.random() * (max - min + 1)); }`);
1287
+ }
1288
+ return `${this.#prefix}Int(${min}, ${max})`;
1289
+ }
1290
+ }
1291
+ // ---------------------------------------------------------------------------
1292
+ // Free helpers
1293
+ // ---------------------------------------------------------------------------
1294
+ /**
1295
+ * The shape half of a scalar check, without its bounds. Shared by `check` and `issues`
1296
+ * so the two cannot drift apart about what a `number` is.
1297
+ *
1298
+ * `NaN` is rejected: it cannot cross a JSON boundary, and the runtime walker has always
1299
+ * rejected it. The emitted form used not to, which is one of the divergences REQ-AV-4
1300
+ * exists to close.
1301
+ */
1302
+ function scalarBase(scalar, v) {
1303
+ switch (scalar) {
1304
+ case 'string':
1305
+ return `typeof ${v} === "string"`;
1306
+ case 'number':
1307
+ return `typeof ${v} === "number" && !Number.isNaN(${v})`;
1308
+ case 'integer':
1309
+ return `Number.isInteger(${v})`;
1310
+ case 'bigint':
1311
+ return `typeof ${v} === "bigint"`;
1312
+ case 'boolean':
1313
+ return `typeof ${v} === "boolean"`;
1314
+ case 'date':
1315
+ return `${v} instanceof Date && !Number.isNaN(${v}.getTime())`;
1316
+ }
1317
+ }
1318
+ /** `.email`, or `["odd name"]` when the property is not a plain identifier. */
1319
+ function accessor(name) {
1320
+ return IDENTIFIER.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
1321
+ }
1322
+ /**
1323
+ * Extend a path *expression* with a static key, folding when it is already a literal so
1324
+ * the emitted code reads `"input.email"` rather than `"input" + ".email"`.
1325
+ *
1326
+ * boundary: `JSON.parse` returns `any`, and the assertion says the parse of a JSON string
1327
+ * literal is a string. `STRING_LITERAL` is what establishes that — it matches a complete
1328
+ * double-quoted JSON string and nothing else, so the parse cannot return a number or an
1329
+ * object. `indexed` below carries the same argument.
1330
+ */
1331
+ function join(pathExpr, key) {
1332
+ const suffix = IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
1333
+ if (STRING_LITERAL.test(pathExpr))
1334
+ return JSON.stringify(`${JSON.parse(pathExpr)}${suffix}`);
1335
+ return `${pathExpr} + ${JSON.stringify(suffix)}`;
1336
+ }
1337
+ /**
1338
+ * Extend a path expression with an index, which for an array is only known at runtime.
1339
+ *
1340
+ * boundary: as in `join` — `STRING_LITERAL` proves the parse yields a string.
1341
+ */
1342
+ function indexed(pathExpr, index) {
1343
+ if (STRING_LITERAL.test(pathExpr) && NUMERIC.test(index)) {
1344
+ return JSON.stringify(`${JSON.parse(pathExpr)}[${index}]`);
1345
+ }
1346
+ return `${pathExpr} + "[" + ${index} + "]"`;
1347
+ }
1348
+ function capitalise(name) {
1349
+ const cleaned = name.replaceAll(/[^A-Za-z0-9]/g, '');
1350
+ if (cleaned.length === 0)
1351
+ return 'T';
1352
+ return `${cleaned.slice(0, 1).toUpperCase()}${cleaned.slice(1)}`;
1353
+ }
1354
+ export { discriminantOf, expectedForConstraint, expectedForDiscriminant, expectedOf, hasExcessCheck, messageFor, } from '@zmdb/schema/ir';
1355
+ export {} from '@zmdb/schema/ir';
1356
+ //# sourceMappingURL=index.js.map