@oh-my-pi/omptype 18.16.0 → 18.18.0

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.
@@ -0,0 +1,495 @@
1
+ /**
2
+ * JIT backend: generates a specialized boolean validator for a {@link Plan}
3
+ * graph via `new Function`.
4
+ *
5
+ * Plans inline into the function that reaches them; `$ref` targets, recursive
6
+ * plans, and repeated non-leaf subtrees become their own functions. Failures
7
+ * jump with `return false` or a labeled `break`, so union branches, `not`, and
8
+ * `if` run inline without allocating closures. Unions whose branches share a
9
+ * required tag property compile to a `switch` on that tag. Plans that need
10
+ * annotation tracking (`unevaluated*`) delegate to the interpreter.
11
+ */
12
+ import { findDiscriminator, isRefOnly, unwrapRef, validate } from "./interpret";
13
+ import {
14
+ kindOf,
15
+ type Plan,
16
+ T_ANY,
17
+ T_ARRAY,
18
+ T_BOOLEAN,
19
+ T_INTEGER,
20
+ T_NULL,
21
+ T_NUMBER,
22
+ T_NUMERIC,
23
+ T_OBJECT,
24
+ T_STRING,
25
+ } from "./plan";
26
+ import { compileSimplePattern } from "./pattern";
27
+ import { codePointLength, isInheritedName, isMultipleOf, jsonEqual, jsonIncludes, uniqueItems } from "./runtime";
28
+
29
+ const HELPERS = {
30
+ eq: jsonEqual,
31
+ inc: jsonIncludes,
32
+ uniq: uniqueItems,
33
+ mult: isMultipleOf,
34
+ cpl: codePointLength,
35
+ own: Object.hasOwn,
36
+ };
37
+
38
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
39
+
40
+ /** Set once code generation from strings is refused (strict CSP); never retried. */
41
+ let codegenBlocked = false;
42
+
43
+ /**
44
+ * Generate a validator for `root`, or `undefined` when the runtime forbids
45
+ * code generation from strings.
46
+ */
47
+ export function generate(root: Plan): ((value: unknown) => value is unknown) | undefined {
48
+ if (codegenBlocked) return undefined;
49
+ const source = new Generator().program(root);
50
+ let factory: (refs: unknown[], helpers: typeof HELPERS) => (value: unknown) => value is unknown;
51
+ try {
52
+ factory = new Function("R", "H", source.code) as typeof factory;
53
+ } catch (error) {
54
+ if (!(error instanceof EvalError)) throw error;
55
+ codegenBlocked = true;
56
+ return undefined;
57
+ }
58
+ return factory(source.refs, HELPERS);
59
+ }
60
+
61
+ function literal(value: unknown): string {
62
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
63
+ }
64
+
65
+ function isPrimitive(value: unknown): boolean {
66
+ return value === null || (typeof value !== "object" && typeof value !== "function");
67
+ }
68
+
69
+ /** Boolean expression: `x` matches `types`. */
70
+ function typeTest(types: number, x: string): string {
71
+ const tests: string[] = [];
72
+ if (types & T_OBJECT) tests.push(`(typeof ${x}==="object"&&${x}!==null&&!Array.isArray(${x}))`);
73
+ if (types & T_ARRAY) tests.push(`Array.isArray(${x})`);
74
+ if (types & T_STRING) tests.push(`typeof ${x}==="string"`);
75
+ if (types & T_NUMBER) tests.push(`Number.isFinite(${x})`);
76
+ else if (types & T_INTEGER) tests.push(`Number.isInteger(${x})`);
77
+ if (types & T_BOOLEAN) tests.push(`typeof ${x}==="boolean"`);
78
+ if (types & T_NULL) tests.push(`${x}===null`);
79
+ return tests.length === 1 ? tests[0] : `(${tests.join("||")})`;
80
+ }
81
+
82
+ /** Boolean expression: `x` does not match `types` (single kinds avoid a negated group). */
83
+ function typeMismatch(types: number, x: string): string {
84
+ switch (types) {
85
+ case T_OBJECT:
86
+ return `typeof ${x}!=="object"||${x}===null||Array.isArray(${x})`;
87
+ case T_ARRAY:
88
+ return `!Array.isArray(${x})`;
89
+ case T_STRING:
90
+ return `typeof ${x}!=="string"`;
91
+ case T_NUMERIC:
92
+ return `!Number.isFinite(${x})`;
93
+ case T_INTEGER:
94
+ return `!Number.isInteger(${x})`;
95
+ case T_BOOLEAN:
96
+ return `typeof ${x}!=="boolean"`;
97
+ case T_NULL:
98
+ return `${x}!==null`;
99
+ default:
100
+ return `!${typeTest(types, x)}`;
101
+ }
102
+ }
103
+
104
+ /** Leaf plans have no subschemas, so inlining them repeatedly stays small. */
105
+ function isLeaf(plan: Plan): boolean {
106
+ return (
107
+ plan.refs === undefined &&
108
+ plan.prefixItems === undefined &&
109
+ plan.items === undefined &&
110
+ plan.contains === undefined &&
111
+ plan.properties === undefined &&
112
+ plan.patternProperties === undefined &&
113
+ plan.additionalProperties === undefined &&
114
+ plan.propertyNames === undefined &&
115
+ plan.dependentSchemas === undefined &&
116
+ plan.allOf === undefined &&
117
+ plan.anyOf === undefined &&
118
+ plan.oneOf === undefined &&
119
+ plan.not === undefined &&
120
+ plan.if === undefined
121
+ );
122
+ }
123
+
124
+ class Generator {
125
+ readonly #refs: unknown[] = [];
126
+ readonly #refIndex = new Map<unknown, number>();
127
+ readonly #functions = new Map<Plan, string>();
128
+ readonly #queue: Plan[] = [];
129
+ readonly #inlined = new Set<Plan>();
130
+ readonly #inlining = new Set<Plan>();
131
+ /** Pattern source → name of its char-code function or `RegExp` ref. */
132
+ readonly #patterns = new Map<string, string>();
133
+ readonly #helpers: string[] = [];
134
+ #counter = 0;
135
+
136
+ program(root: Plan): { code: string; refs: unknown[] } {
137
+ const entry = this.#fn(root);
138
+ const functions: string[] = [];
139
+ for (let index = 0; index < this.#queue.length; index++) {
140
+ const plan = this.#queue[index];
141
+ this.#inlining.add(plan);
142
+ const body = this.#emitInline(plan, "x", "return false", T_ANY);
143
+ this.#inlining.delete(plan);
144
+ functions.push(`function ${this.#functions.get(plan)}(x){${body}return true}`);
145
+ }
146
+ let prelude = "const{eq,inc,uniq,mult,cpl,own}=H;";
147
+ for (let index = 0; index < this.#refs.length; index++) prelude += `const r${index}=R[${index}];`;
148
+ return { code: `${prelude}${this.#helpers.join("")}${functions.join("")}return ${entry};`, refs: this.#refs };
149
+ }
150
+
151
+ #name(prefix: string): string {
152
+ return `${prefix}${this.#counter++}`;
153
+ }
154
+
155
+ #ref(value: unknown): string {
156
+ let index = this.#refIndex.get(value);
157
+ if (index === undefined) {
158
+ index = this.#refs.length;
159
+ this.#refs.push(value);
160
+ this.#refIndex.set(value, index);
161
+ }
162
+ return `r${index}`;
163
+ }
164
+
165
+ #fn(plan: Plan): string {
166
+ let name = this.#functions.get(plan);
167
+ if (name === undefined) {
168
+ name = `f${this.#functions.size}`;
169
+ this.#functions.set(plan, name);
170
+ this.#queue.push(plan);
171
+ }
172
+ return name;
173
+ }
174
+
175
+ /** Code that runs `fail` unless `x` satisfies `plan`; `known` is a type mask `x` already satisfies. */
176
+ #emit(plan: Plan, x: string, fail: string, known: number): string {
177
+ if (plan.types === 0) return `${fail};`;
178
+ if (plan.empty) return "";
179
+ if (this.#inlining.has(plan) || (this.#inlined.has(plan) && !isLeaf(plan))) {
180
+ return `if(!${this.#fn(plan)}(${x}))${fail};`;
181
+ }
182
+ this.#inlined.add(plan);
183
+ this.#inlining.add(plan);
184
+ const code = this.#emitInline(plan, x, fail, known);
185
+ this.#inlining.delete(plan);
186
+ return code;
187
+ }
188
+
189
+ #emitInline(plan: Plan, x: string, fail: string, known: number): string {
190
+ if (plan.types === 0) return `${fail};`;
191
+ if (plan.empty) return "";
192
+ if (plan.unevaluatedProperties !== undefined || plan.unevaluatedItems !== undefined) {
193
+ const check = (value: unknown): boolean => validate(plan, value);
194
+ return `if(!${this.#ref(check)}(${x}))${fail};`;
195
+ }
196
+ if (isRefOnly(plan)) return this.#emitRefs(plan.refs!, x, fail);
197
+
198
+ let out = "";
199
+ let kind = known;
200
+ const types = plan.types;
201
+ if (types !== T_ANY && (known === 0 || (types & known) !== known)) {
202
+ out += `if(${typeMismatch(types, x)})${fail};`;
203
+ kind = kindOf(types);
204
+ }
205
+ if (plan.hasConst) {
206
+ out += isPrimitive(plan.constValue)
207
+ ? `if(${x}!==${literal(plan.constValue)})${fail};`
208
+ : `if(!eq(${x},${this.#ref(plan.constValue)}))${fail};`;
209
+ }
210
+ if (plan.enumValues !== undefined) out += this.#emitEnum(plan.enumValues, x, fail);
211
+ out += this.#emitNumber(plan, x, fail, kind);
212
+ out += this.#emitString(plan, x, fail, kind);
213
+ out += this.#emitArray(plan, x, fail, kind);
214
+ out += this.#emitObject(plan, x, fail, kind);
215
+ if (plan.refs !== undefined) out += this.#emitRefs(plan.refs, x, fail);
216
+ if (plan.allOf !== undefined) for (const branch of plan.allOf) out += this.#emit(branch, x, fail, kind);
217
+ if (plan.anyOf !== undefined) out += this.#emitUnion(plan.anyOf, x, fail, kind, false);
218
+ if (plan.oneOf !== undefined) out += this.#emitUnion(plan.oneOf, x, fail, kind, true);
219
+ if (plan.not !== undefined) {
220
+ const label = this.#name("L");
221
+ out += `${label}:{${this.#emit(plan.not, x, `break ${label}`, kind)}${fail};}`;
222
+ }
223
+ if (plan.if !== undefined && (plan.ifThen !== undefined || plan.ifElse !== undefined)) {
224
+ const done = this.#name("L");
225
+ const otherwise = this.#name("L");
226
+ const then = plan.ifThen === undefined ? "" : this.#emit(plan.ifThen, x, fail, kind);
227
+ const elseCode = plan.ifElse === undefined ? "" : this.#emit(plan.ifElse, x, fail, kind);
228
+ out += `${done}:{${otherwise}:{${this.#emit(plan.if, x, `break ${otherwise}`, kind)}${then}break ${done};}${elseCode}}`;
229
+ }
230
+ return out;
231
+ }
232
+
233
+ #emitRefs(refs: readonly Plan[], x: string, fail: string): string {
234
+ let out = "";
235
+ for (const ref of refs) {
236
+ const target = unwrapRef(ref);
237
+ if (target.types === 0) return `${fail};`;
238
+ if (target.empty) continue;
239
+ out += `if(!${this.#fn(target)}(${x}))${fail};`;
240
+ }
241
+ return out;
242
+ }
243
+
244
+ #emitEnum(values: readonly unknown[], x: string, fail: string): string {
245
+ if (!values.every(isPrimitive)) return `if(!inc(${this.#ref(values)},${x}))${fail};`;
246
+ if (values.length === 0) return `${fail};`;
247
+ if (values.length <= 8) return `if(${values.map(value => `${x}!==${literal(value)}`).join("&&")})${fail};`;
248
+ return `if(!${this.#ref(new Set(values))}.has(${x}))${fail};`;
249
+ }
250
+
251
+ #emitNumber(plan: Plan, x: string, fail: string, kind: number): string {
252
+ let out = "";
253
+ if (plan.minimum !== undefined) out += `if(${x}<${plan.minimum})${fail};`;
254
+ if (plan.exclusiveMinimum !== undefined) out += `if(${x}<=${plan.exclusiveMinimum})${fail};`;
255
+ if (plan.maximum !== undefined) out += `if(${x}>${plan.maximum})${fail};`;
256
+ if (plan.exclusiveMaximum !== undefined) out += `if(${x}>=${plan.exclusiveMaximum})${fail};`;
257
+ if (plan.multipleOf !== undefined) {
258
+ out += Number.isInteger(plan.multipleOf)
259
+ ? `if(${x}%${plan.multipleOf}!==0)${fail};`
260
+ : `if(!mult(${x},${plan.multipleOf}))${fail};`;
261
+ }
262
+ if (out === "" || (kind !== 0 && (kind & T_NUMERIC) === kind)) return out;
263
+ return `if(typeof ${x}==="number"){${out}}`;
264
+ }
265
+
266
+ #emitString(plan: Plan, x: string, fail: string, kind: number): string {
267
+ let out = "";
268
+ const { minLength, maxLength } = plan;
269
+ if (minLength !== undefined && minLength > 0) {
270
+ out +=
271
+ minLength === 1
272
+ ? `if(${x}.length<1)${fail};`
273
+ : `if(${x}.length<${minLength * 2}&&cpl(${x})<${minLength})${fail};`;
274
+ }
275
+ if (maxLength !== undefined) out += `if(${x}.length>${maxLength}&&cpl(${x})>${maxLength})${fail};`;
276
+ if (plan.pattern !== undefined) out += `if(!${this.#pattern(plan.pattern)}(${x}))${fail};`;
277
+ if (plan.format !== undefined) out += `if(!${this.#ref(plan.format)}(${x}))${fail};`;
278
+ if (out === "" || kind === T_STRING) return out;
279
+ return `if(typeof ${x}==="string"){${out}}`;
280
+ }
281
+
282
+ /** Callee testing a string against `pattern`: a char-code function when simple, else the regex's `test`. */
283
+ #pattern(pattern: RegExp): string {
284
+ let callee = this.#patterns.get(pattern.source);
285
+ if (callee === undefined) {
286
+ const name = this.#name("pat");
287
+ const helper = compileSimplePattern(pattern.source, name);
288
+ if (helper === undefined) {
289
+ callee = `${this.#ref(pattern)}.test`;
290
+ } else {
291
+ this.#helpers.push(helper);
292
+ callee = name;
293
+ }
294
+ this.#patterns.set(pattern.source, callee);
295
+ }
296
+ return callee;
297
+ }
298
+
299
+ #emitArray(plan: Plan, x: string, fail: string, kind: number): string {
300
+ const n = this.#name("n");
301
+ let out = "";
302
+ if (plan.minItems !== undefined && plan.minItems > 0) out += `if(${n}<${plan.minItems})${fail};`;
303
+ if (plan.maxItems !== undefined) out += `if(${n}>${plan.maxItems})${fail};`;
304
+ const prefix = plan.prefixItems ?? [];
305
+ for (let index = 0; index < prefix.length; index++) {
306
+ const item = prefix[index];
307
+ if (item.empty) continue;
308
+ const element = this.#name("e");
309
+ out += `if(${n}>${index}){const ${element}=${x}[${index}];${this.#emit(item, element, fail, 0)}}`;
310
+ }
311
+ if (plan.items !== undefined && !plan.items.empty) {
312
+ if (plan.items.types === 0) {
313
+ out += `if(${n}>${prefix.length})${fail};`;
314
+ } else {
315
+ const index = this.#name("i");
316
+ const element = this.#name("e");
317
+ out += `for(let ${index}=${prefix.length};${index}<${n};${index}++){const ${element}=${x}[${index}];${this.#emit(plan.items, element, fail, 0)}}`;
318
+ }
319
+ }
320
+ if (plan.contains !== undefined) {
321
+ const atLeast = plan.minContains ?? 1;
322
+ const atMost = plan.maxContains;
323
+ if (atLeast > 0 || atMost !== undefined) {
324
+ const count = this.#name("c");
325
+ const index = this.#name("i");
326
+ const label = this.#name("L");
327
+ const element = `${x}[${index}]`;
328
+ const early = atMost === undefined ? `if(${count}>=${atLeast})break;` : "";
329
+ out += `let ${count}=0;for(let ${index}=0;${index}<${n};${index}++){${label}:{${this.#emit(plan.contains, element, `break ${label}`, 0)}${count}++;${early}}}`;
330
+ if (atLeast > 0) out += `if(${count}<${atLeast})${fail};`;
331
+ if (atMost !== undefined) out += `if(${count}>${atMost})${fail};`;
332
+ }
333
+ }
334
+ if (plan.uniqueItems) out += `if(!uniq(${x}))${fail};`;
335
+ if (out === "") return out;
336
+ out = `const ${n}=${x}.length;${out}`;
337
+ return kind === T_ARRAY ? out : `if(Array.isArray(${x})){${out}}`;
338
+ }
339
+
340
+ #get(x: string, key: string): string {
341
+ const access = IDENTIFIER.test(key) ? `${x}.${key}` : `${x}[${JSON.stringify(key)}]`;
342
+ return isInheritedName(key) ? `(own(${x},${JSON.stringify(key)})?${access}:undefined)` : access;
343
+ }
344
+
345
+ #emitObject(plan: Plan, x: string, fail: string, kind: number): string {
346
+ let out = "";
347
+ const required = new Set(plan.required);
348
+ const properties = plan.properties ?? [];
349
+ const declared = new Set(properties.map(property => property.key));
350
+ for (const key of required) {
351
+ if (!declared.has(key)) out += `if(${this.#get(x, key)}===undefined)${fail};`;
352
+ }
353
+ // Closed objects whose required keys are all declared: when the key count equals the number of
354
+ // declared keys present, no other key exists; otherwise a slow pass finds the undeclared one.
355
+ const additional = plan.additionalProperties;
356
+ const counted =
357
+ additional !== undefined &&
358
+ additional.types === 0 &&
359
+ plan.patternProperties === undefined &&
360
+ (plan.propertyNames === undefined || plan.propertyNames.empty) &&
361
+ plan.minProperties === undefined &&
362
+ plan.maxProperties === undefined &&
363
+ [...required].every(key => declared.has(key));
364
+ const present = this.#name("pc");
365
+ let optional = false;
366
+ for (const property of properties) {
367
+ const isRequired = required.has(property.key);
368
+ if (property.plan.empty) {
369
+ if (isRequired) out += `if(${this.#get(x, property.key)}===undefined)${fail};`;
370
+ else if (counted) {
371
+ out += `if(${this.#get(x, property.key)}!==undefined)${present}++;`;
372
+ optional = true;
373
+ }
374
+ continue;
375
+ }
376
+ const value = this.#name("p");
377
+ const check = this.#emit(property.plan, value, fail, 0);
378
+ out += `const ${value}=${this.#get(x, property.key)};`;
379
+ if (isRequired) out += `if(${value}===undefined)${fail};${check}`;
380
+ else {
381
+ out += `if(${value}!==undefined){${counted ? `${present}++;` : ""}${check}}`;
382
+ optional ||= counted;
383
+ }
384
+ }
385
+ if (counted) {
386
+ const key = this.#name("k");
387
+ const count = this.#name("c");
388
+ const expected = optional ? present : String(required.size);
389
+ if (optional) out = `let ${present}=${required.size};${out}`;
390
+ out += `let ${count}=0;for(const ${key} in ${x})${count}++;`;
391
+ out += `if(${count}!==${expected}){for(const ${key} in ${x})if(!${this.#isDeclared(key, declared)})${fail};}`;
392
+ } else {
393
+ out += this.#emitKeys(plan, x, fail, declared);
394
+ }
395
+ if (plan.dependentRequired !== undefined) {
396
+ for (const dependency of plan.dependentRequired) {
397
+ const missing = dependency.required.map(key => `${this.#get(x, key)}===undefined`).join("||");
398
+ out += `if(${this.#get(x, dependency.key)}!==undefined&&(${missing}))${fail};`;
399
+ }
400
+ }
401
+ if (plan.dependentSchemas !== undefined) {
402
+ for (const dependency of plan.dependentSchemas) {
403
+ out += `if(${this.#get(x, dependency.key)}!==undefined){${this.#emit(dependency.plan, x, fail, T_OBJECT)}}`;
404
+ }
405
+ }
406
+ if (out === "" || kind === T_OBJECT) return out;
407
+ return `if(typeof ${x}==="object"&&${x}!==null&&!Array.isArray(${x})){${out}}`;
408
+ }
409
+
410
+ /** The `for…in` pass: `patternProperties`, `additionalProperties`, `propertyNames`, property counts. */
411
+ #emitKeys(plan: Plan, x: string, fail: string, declared: Set<string>): string {
412
+ const patterns = plan.patternProperties;
413
+ const additional = plan.additionalProperties;
414
+ const names = plan.propertyNames;
415
+ const { minProperties, maxProperties } = plan;
416
+ const counting = minProperties !== undefined || maxProperties !== undefined;
417
+ const checkAdditional = additional !== undefined && !additional.empty;
418
+ const checkNames = names !== undefined && !names.empty;
419
+ if (patterns === undefined && !checkAdditional && !checkNames && !counting) return "";
420
+ const key = this.#name("k");
421
+ const count = this.#name("c");
422
+ let body = "";
423
+ if (counting) body += `${count}++;`;
424
+ if (checkNames) body += this.#emit(names, key, fail, T_STRING);
425
+ if (patterns !== undefined || checkAdditional) {
426
+ const matched = this.#name("m");
427
+ const tracking = checkAdditional && (patterns !== undefined || declared.size > 0);
428
+ if (tracking) body += `let ${matched}=${this.#isDeclared(key, declared)};`;
429
+ for (const { pattern, plan: patternPlan } of patterns ?? []) {
430
+ const value = this.#name("v");
431
+ const check = patternPlan.empty
432
+ ? ""
433
+ : `const ${value}=${x}[${key}];${this.#emit(patternPlan, value, fail, 0)}`;
434
+ const mark = tracking ? `${matched}=true;` : "";
435
+ if (check !== "" || mark !== "") body += `if(${this.#ref(pattern)}.test(${key})){${mark}${check}}`;
436
+ }
437
+ if (checkAdditional) {
438
+ const value = this.#name("v");
439
+ const check =
440
+ additional.types === 0
441
+ ? `${fail};`
442
+ : `const ${value}=${x}[${key}];${this.#emit(additional, value, fail, 0)}`;
443
+ body += tracking ? `if(!${matched}){${check}}` : `{${check}}`;
444
+ }
445
+ }
446
+ let out = `for(const ${key} in ${x}){${body}}`;
447
+ if (counting) {
448
+ out = `let ${count}=0;${out}`;
449
+ if (minProperties !== undefined) out += `if(${count}<${minProperties})${fail};`;
450
+ if (maxProperties !== undefined) out += `if(${count}>${maxProperties})${fail};`;
451
+ }
452
+ return out;
453
+ }
454
+
455
+ #isDeclared(key: string, declared: Set<string>): string {
456
+ if (declared.size === 0) return "false";
457
+ if (declared.size > 8) return `${this.#ref(declared)}.has(${key})`;
458
+ return `(${[...declared].map(name => `${key}===${JSON.stringify(name)}`).join("||")})`;
459
+ }
460
+
461
+ #emitUnion(branches: readonly Plan[], x: string, fail: string, kind: number, exclusive: boolean): string {
462
+ const discriminator = findDiscriminator(branches);
463
+ if (discriminator !== undefined) {
464
+ const byBranch = new Map<number, unknown[]>();
465
+ for (const [tag, index] of discriminator.branches) {
466
+ const tags = byBranch.get(index);
467
+ if (tags === undefined) byBranch.set(index, [tag]);
468
+ else tags.push(tag);
469
+ }
470
+ let cases = "";
471
+ for (const [index, tags] of byBranch) {
472
+ cases += `${tags.map(tag => `case ${literal(tag)}:`).join("")}{${this.#emit(branches[index], x, fail, T_OBJECT)}break;}`;
473
+ }
474
+ const guard = kind === T_OBJECT ? "" : `if(${typeMismatch(T_OBJECT, x)})${fail};`;
475
+ return `${guard}switch(${this.#get(x, discriminator.key)}){${cases}default:${fail};}`;
476
+ }
477
+ if (!exclusive && branches.some(branch => branch.empty)) return "";
478
+ if (exclusive) {
479
+ const count = this.#name("c");
480
+ let out = `let ${count}=0;`;
481
+ for (const branch of branches) {
482
+ const label = this.#name("L");
483
+ out += `${label}:{${this.#emit(branch, x, `break ${label}`, kind)}if(++${count}>1)${fail};}`;
484
+ }
485
+ return `${out}if(${count}===0)${fail};`;
486
+ }
487
+ const done = this.#name("L");
488
+ let out = `${done}:{`;
489
+ for (const branch of branches) {
490
+ const label = this.#name("L");
491
+ out += `${label}:{${this.#emit(branch, x, `break ${label}`, kind)}break ${done};}`;
492
+ }
493
+ return `${out}${fail};}`;
494
+ }
495
+ }