zod-compiler 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Compile Zod schemas into zero-overhead validation functions at build time.**
4
4
 
5
- Keep your existing Zod schemas. Get **up to 42x faster** validation, and up to **47x** on rejected
5
+ Keep your existing Zod schemas. Get **up to 44x faster** validation, and up to **46x** on rejected
6
6
  input. No code changes required.
7
7
 
8
8
  Requires **Zod ≥ 4.5**. Compiled output reproduces 4.5's semantics exactly, down to code-point string
@@ -26,7 +26,7 @@ loads pre-generated validators.
26
26
  | | zod-compiler (build plugins / CLI) | Zod `z.compile()` |
27
27
  | -------------------------------------- | -------------------------------------------- | --------------------------------------------- |
28
28
  | Compilation | Build time (true AOT) | Runtime (`z.compile()` or the first parse) |
29
- | Reported validation speedup | Up to 42x; up to 47x on rejected input | ~9x in Zod's headline example |
29
+ | Reported validation speedup | Up to 44x; up to 46x on rejected input | ~9x in Zod's headline example |
30
30
  | Uses `new Function()` at runtime\* | No | Yes |
31
31
  | Cold start | Fast; the validator is already generated | Pays for code generation at startup/first use |
32
32
  | Strict CSP without `'unsafe-eval'` | Supported | Compilation is unavailable |
@@ -543,7 +543,7 @@ npx zod-compiler check src/schemas.ts --json --fail-under 80
543
543
 
544
544
  ## What Gets Compiled
545
545
 
546
- ### Fully Compiled (up to 42x faster)
546
+ ### Fully Compiled (up to 44x faster)
547
547
 
548
548
  Every Zod type except the fallbacks below: all primitives, `object` / `strictObject` / `looseObject`,
549
549
  `array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe`,
@@ -594,52 +594,52 @@ Schema-level `error` and `z.config()` maps are unaffected; for a per-call map us
594
594
 
595
595
  | Scenario | Zod v3 | Zod v4 | **zod-compiler** | Typia | AJV | vs Zod v4 |
596
596
  | ----------------------------------------------- | ------ | ------ | ---------------- | ----- | ----- | --------- |
597
- | simple string | 12.7M | 15.8M | **16.6M** | 17.4M | 17.4M | 1.0x |
598
- | string (min/max) | 11.9M | 7.4M | **17.5M** | 17.3M | 15.6M | 2.4x |
599
- | number (int+positive) | 12.0M | 9.6M | **16.9M** | 16.9M | 16.9M | 1.8x |
600
- | enum | 11.2M | 15.4M | **17.4M** | 16.9M | 17.0M | 1.1x |
601
- | bigint (min/max) | 11.8M | 8.3M | **17.0M** | — | — | 2.1x |
602
- | tuple [string, int, bool] | 5.7M | 7.6M | **17.9M** | 16.8M | 16.0M | 2.4x |
603
- | record\<string, number\> | 3.1M | 2.5M | **12.6M** | 12.0M | 15.3M | 5.0x |
604
- | set\<string\> (5 items) | 3.6M | 2.2M | **16.0M** | — | — | 7.2x |
605
- | set\<string\> (20 items) | 1.3M | 661K | **12.4M** | — | — | **19x** |
606
- | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.6M** | — | — | **11x** |
607
- | map\<string, number\> (20 entries) | 618K | 338K | **8.7M** | — | — | **26x** |
608
- | pipe (non-transform) | 8.9M | 4.7M | **17.3M** | — | — | 3.7x |
609
- | discriminatedUnion (3 variants) | 3.3M | 5.2M | **16.2M** | 15.9M | 7.5M | 3.1x |
610
- | discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **9.5M** | — | — | 2.2x |
611
- | plain union of 8 tagged objects (auto-discrim.) | 348K | 1.3M | **10.2M** | — | — | 8.1x |
612
- | strict object (DB row) | 1.7M | 3.0M | **11.1M** | — | — | 3.6x |
613
- | medium object (valid) | 1.9M | 2.4M | **9.9M** | 10.8M | 7.5M | 4.2x |
614
- | medium object (extra keys stripped) | 1.8M | 2.1M | **9.8M** | — | — | 4.7x |
615
- | medium object (invalid) | 511K | 359K | **15.7M** | 2.9M | 7.5M | **44x** |
616
- | large object (10 items) | 117K | 175K | **5.3M** | 5.7M | 1.2M | **30x** |
617
- | large object (100 items) | 13K | 18K | **779K** | 1.3M | 127K | **42x** |
618
- | readonly field (wrapper compiles away) | 3.0M | 6.6M | **15.7M** | — | — | 2.4x |
619
- | readonly root object (rebuild + freeze) | 2.8M | 5.4M | **12.8M** | — | — | 2.4x |
620
- | readonly array (delegates to Zod) | 3.8M | 4.2M | **4.1M** | — | — | 1.0x |
621
- | recursive tree (7 nodes) | 564K | 994K | **7.9M** | 12.1M | 4.7M | 7.9x |
622
- | recursive tree (121 nodes) | 31K | 55K | **783K** | 1.9M | 361K | **14x** |
623
- | nested recursion (7 nodes) | 383K | 667K | **7.9M** | 11.2M | 2.9M | **12x** |
624
- | nested recursion (121 nodes) | 23K | 41K | **817K** | 1.6M | 206K | **20x** |
625
- | deeply nested object (243 leaves) | 11K | 27K | **803K** | 1.0M | 122K | **30x** |
626
- | event log (combined) | 371K | 782K | **7.5M** | — | — | 9.6x |
627
- | object with transform (zero-capture) | 1.1M | 2.0M | **7.2M** | — | — | 3.7x |
628
- | array 10 × transform (zero-capture) | 122K | 203K | **4.2M** | — | — | **21x** |
629
- | array 50 × transform (zero-capture) | 25K | 42K | **1.0M** | — | — | **25x** |
630
- | object with captured transform | 1.2M | 8.2M | **16.2M** | — | — | 2.0x |
631
- | object with captured refine (cross-field) | 1.4M | 2.2M | **11.2M** | — | — | 5.0x |
632
- | object with superRefine (cross-field) | 1.4M | 2.2M | **9.3M** | — | — | 4.3x |
633
- | coerced query object (valid) | 1.8M | 2.9M | **5.4M** | — | — | 1.9x |
634
- | coerced query object (invalid) | 1.0M | 828K | **10.3M** | — | — | **12x** |
635
- | preprocessed query object (valid) | 401K | 1.7M | **5.2M** | — | — | 3.0x |
636
- | preprocessed query object (invalid) | 378K | 755K | **12.7M** | — | — | **17x** |
597
+ | simple string | 12.6M | 15.6M | **16.8M** | 17.2M | 17.4M | 1.1x |
598
+ | string (min/max) | 12.4M | 7.4M | **17.4M** | 17.6M | 15.5M | 2.4x |
599
+ | number (int+positive) | 11.8M | 9.4M | **17.2M** | 16.3M | 17.4M | 1.8x |
600
+ | enum | 12.1M | 14.7M | **17.4M** | 17.4M | 17.2M | 1.2x |
601
+ | bigint (min/max) | 11.2M | 7.9M | **16.7M** | — | — | 2.1x |
602
+ | tuple [string, int, bool] | 5.6M | 7.3M | **17.4M** | 16.4M | 15.7M | 2.4x |
603
+ | record\<string, number\> | 3.1M | 2.6M | **12.7M** | 12.0M | 15.2M | 5.0x |
604
+ | set\<string\> (5 items) | 3.7M | 2.3M | **15.2M** | — | — | 6.6x |
605
+ | set\<string\> (20 items) | 1.3M | 680K | **12.1M** | — | — | **18x** |
606
+ | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.2M** | — | — | **10x** |
607
+ | map\<string, number\> (20 entries) | 637K | 347K | **8.5M** | — | — | **24x** |
608
+ | pipe (non-transform) | 8.7M | 4.7M | **17.3M** | — | — | 3.6x |
609
+ | discriminatedUnion (3 variants) | 3.3M | 5.2M | **17.2M** | 16.1M | 7.8M | 3.3x |
610
+ | discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **10.3M** | — | — | 2.3x |
611
+ | plain union of 8 tagged objects (auto-discrim.) | 356K | 1.3M | **10.2M** | — | — | 7.7x |
612
+ | strict object (DB row) | 1.8M | 3.0M | **11.3M** | — | — | 3.8x |
613
+ | medium object (valid) | 1.9M | 2.2M | **9.4M** | 11.2M | 7.5M | 4.2x |
614
+ | medium object (extra keys stripped) | 1.8M | 2.0M | **9.8M** | — | — | 4.9x |
615
+ | medium object (invalid) | 534K | 372K | **15.5M** | 2.9M | 7.5M | **42x** |
616
+ | large object (10 items) | 120K | 163K | **5.1M** | 5.9M | 1.2M | **31x** |
617
+ | large object (100 items) | 13K | 17K | **766K** | 1.3M | 127K | **44x** |
618
+ | readonly field (wrapper compiles away) | 3.0M | 6.6M | **16.7M** | — | — | 2.5x |
619
+ | readonly root object (rebuild + freeze) | 2.8M | 5.5M | **12.9M** | — | — | 2.3x |
620
+ | readonly array (delegates to Zod) | 3.9M | 4.3M | **4.2M** | — | — | 1.0x |
621
+ | recursive tree (7 nodes) | 581K | 1.0M | **7.5M** | 11.6M | 4.7M | 7.4x |
622
+ | recursive tree (121 nodes) | 31K | 57K | **777K** | 1.9M | 371K | **14x** |
623
+ | nested recursion (7 nodes) | 395K | 683K | **7.9M** | 11.2M | 3.0M | **12x** |
624
+ | nested recursion (121 nodes) | 24K | 42K | **805K** | 1.6M | 204K | **19x** |
625
+ | deeply nested object (243 leaves) | 11K | 27K | **825K** | 1.0M | 125K | **30x** |
626
+ | event log (combined) | 363K | 795K | **7.3M** | — | — | 9.2x |
627
+ | object with transform (zero-capture) | 1.1M | 1.9M | **6.7M** | — | — | 3.5x |
628
+ | array 10 × transform (zero-capture) | 121K | 206K | **4.1M** | — | — | **20x** |
629
+ | array 50 × transform (zero-capture) | 26K | 41K | **1.0M** | — | — | **25x** |
630
+ | object with captured transform | 1.2M | 7.9M | **15.9M** | — | — | 2.0x |
631
+ | object with captured refine (cross-field) | 1.4M | 2.2M | **11.5M** | — | — | 5.2x |
632
+ | object with superRefine (cross-field) | 1.4M | 2.1M | **9.2M** | — | — | 4.3x |
633
+ | coerced query object (valid) | 1.8M | 2.9M | **5.3M** | — | — | 1.9x |
634
+ | coerced query object (invalid) | 1.0M | 826K | **10.3M** | — | — | **12x** |
635
+ | preprocessed query object (valid) | 433K | 1.7M | **5.3M** | — | — | 3.2x |
636
+ | preprocessed query object (invalid) | 392K | 782K | **12.3M** | — | — | **16x** |
637
637
  | stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
638
- | stringbool config object (invalid) | — | 670K | **13.8M** | — | — | **21x** |
639
- | custom/instanceof request (valid) | 965K | 3.1M | **10.4M** | — | — | 3.3x |
640
- | custom/instanceof request (invalid) | 784K | 930K | **10.2M** | — | — | **11x** |
641
- | disjoint object intersection (valid) | 1.4M | 1.6M | **9.8M** | — | — | 6.0x |
642
- | disjoint object intersection (invalid) | 493K | 327K | **15.4M** | — | — | **47x** |
638
+ | stringbool config object (invalid) | — | 682K | **13.2M** | — | — | **19x** |
639
+ | custom/instanceof request (valid) | 965K | 3.1M | **9.9M** | — | — | 3.2x |
640
+ | custom/instanceof request (invalid) | 786K | 928K | **13.2M** | — | — | **14x** |
641
+ | disjoint object intersection (valid) | 1.4M | 1.6M | **9.5M** | — | — | 5.9x |
642
+ | disjoint object intersection (invalid) | 488K | 331K | **15.3M** | — | — | **46x** |
643
643
 
644
644
  _ops/s, higher is better. `vp test bench` on an Apple M4 Max (zod 4.5.2, zod v3 3.23.8, typia 12, ajv 8),
645
645
  best of three runs. The harness costs ~60 ns per iteration, so the fastest rows sit at that floor and gaps
@@ -662,10 +662,12 @@ failure, and is deferred until `.error` is read. A `z.object()` strips, so it in
662
662
  single pass that validates and rebuilds together and bails on the first failure, covering the reshaping
663
663
  idioms too (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
664
664
 
665
- Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first, discriminated unions
666
- dispatch through a `switch` on the tag (plain tagged unions are auto-discriminated into it, on the
667
- stripping pass as well as the fast check), and oversized check functions are split to stay within V8's
668
- optimizer budget. `z.email()` runs as a single linear scan instead of a backtracking regex, a record's
665
+ Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first on both passes (so a
666
+ payload with a wrong boolean is rejected before its email is scanned, whichever was declared first),
667
+ discriminated unions dispatch through a `switch` on the tag (plain tagged unions are auto-discriminated
668
+ into it, on the stripping pass as well as the fast check, and the selected option re-checks neither
669
+ object-ness nor the tag), and oversized check functions are split to stay within V8's optimizer budget.
670
+ `.is()` stays a zero-allocation predicate on schemas with `.default()` fields. `z.email()` runs as a single linear scan instead of a backtracking regex, a record's
669
671
  plain-object guard exits on one comparison for an ordinary object, and a case-insensitive `stringbool`
670
672
  looks its input up verbatim before paying for `toLowerCase()`. Stripping objects, native coercions,
671
673
  `stringbool`, defaults, string rewrites, context-free preprocessors and synchronous transforms validate
@@ -1 +1 @@
1
- {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBAyLgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
1
+ {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBAuMgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
@@ -2,7 +2,7 @@ import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant,
2
2
  import { ZC_HOP_DECL, ZC_PLAIN_DECL, ZC_PROTO_SCRUB_DECL } from "./issue-decls.js";
3
3
  import { defaultValueExpr, needsPostInnerDefault } from "./schemas/default.js";
4
4
  import { detectUnionDiscriminator } from "./schemas/discriminated-union.js";
5
- import { estimateFastCost, predictedInlineSize } from "./fast-size.js";
5
+ import { estimateFastCost, orderByRuntimeCost, predictedInlineSize } from "./fast-size.js";
6
6
  import { parsedProperties } from "./schemas/object.js";
7
7
  import { innerAppliesDefaultOnUndefined } from "./schemas/optional.js";
8
8
  import { fastStringCheck } from "./schemas/string.js";
@@ -220,7 +220,11 @@ function emitFailSentinel(ctx) {
220
220
  * and 354 KB on the deep fixtures — far past the bytecode size where V8 stops
221
221
  * running TurboFan on it, which would forfeit the speed this path exists for.
222
222
  */
223
- function build(ir, input, g) {
223
+ function build(ir, input, caller) {
224
+ const g = caller.discSkipKey !== void 0 && ir.type !== "object" && ir.type !== "readonly" && ir.type !== "zodDelegate" ? {
225
+ ...caller,
226
+ discSkipKey: void 0
227
+ } : caller;
224
228
  if (ir.type === "recursiveRef") return buildRecursiveCall(ir.refId ?? 0, input, g);
225
229
  if (ir.type === "recursionTarget") return buildRecursionTarget(ir, input, g);
226
230
  if (!g.rebuilds.has(ir)) return passthrough(ir, input, g);
@@ -246,8 +250,56 @@ function build(ir, input, g) {
246
250
  if (out !== null) g.scope.used = before + out.code.length;
247
251
  return out;
248
252
  }
253
+ /**
254
+ * The order an object's properties are validated in: cheapest first, as the
255
+ * fast path orders its conjuncts (see estimateRuntimeCost), so rejected input
256
+ * is decided by a type guard rather than by the most expensive check that
257
+ * happens to be declared first — `safeParse` on a payload with a wrong boolean
258
+ * used to cost the email scan before it looked at the boolean. Accepted input
259
+ * runs every property whatever the order, and the output literal is assembled
260
+ * in shape order regardless, so a passing parse is unchanged.
261
+ *
262
+ * Properties whose parse can be OBSERVED to run are pinned: they keep their
263
+ * declaration order among themselves and run after every free property, so
264
+ * two transforms sharing state still fire in zod's order on a successful
265
+ * parse, and a rejected one fires neither (a failing parse never guaranteed
266
+ * that its effects ran — this pass bails at the first bad check). What counts
267
+ * as observable is user code and value construction: a transform or
268
+ * preprocess, a `.default()` (a function default is invoked on read), a
269
+ * `.catch()`, a coercion (`Number(x)` reaches a `valueOf`), an overwrite, a
270
+ * delegate, and a recursion edge, whose target is not inspected. A `.refine()`
271
+ * is not pinned — it is a predicate, and the fast path already reorders it.
272
+ */
273
+ function orderedForBuild(properties, ctx) {
274
+ const pinned = properties.filter(([, propIR]) => orderSensitive(propIR));
275
+ if (pinned.length === properties.length) return properties;
276
+ return [...orderByRuntimeCost(properties.filter(([, propIR]) => !orderSensitive(propIR)), ([, propIR]) => propIR, ctx), ...pinned];
277
+ }
278
+ /** Does running this subtree do anything a caller could observe besides the verdict? */
279
+ function orderSensitive(ir) {
280
+ switch (ir.type) {
281
+ case "effect":
282
+ case "default":
283
+ case "catch":
284
+ case "fallback":
285
+ case "zodDelegate":
286
+ case "recursiveRef":
287
+ case "recursionTarget": return true;
288
+ case "string":
289
+ if (ir.coerce === true || ir.checks.some((c) => c.kind === "overwrite_effect")) return true;
290
+ break;
291
+ case "number":
292
+ case "boolean":
293
+ case "bigint":
294
+ case "date":
295
+ if (ir.coerce === true) return true;
296
+ break;
297
+ default: break;
298
+ }
299
+ return children(ir).some(orderSensitive);
300
+ }
249
301
  /** Host `ir`'s build in its own function over a fresh parameter; returns its name. */
250
- function hostBuild(ir, g) {
302
+ function hostBuild(ir, g, discSkipKey) {
251
303
  const param = emitTemp(g.ctx, "bp");
252
304
  const scope = {
253
305
  temps: [],
@@ -256,7 +308,8 @@ function hostBuild(ir, g) {
256
308
  const inner = build(ir, param, {
257
309
  ...g,
258
310
  extractable: false,
259
- scope
311
+ scope,
312
+ discSkipKey
260
313
  });
261
314
  if (inner === null) return null;
262
315
  const name = emitTemp(g.ctx, "vb");
@@ -415,7 +468,7 @@ function buildDispatch(discriminator, cases, options, input, g) {
415
468
  if (fn === void 0) {
416
469
  const option = options[index];
417
470
  if (option === void 0) return null;
418
- const hosted = g.rebuilds.has(option) ? hostBuild(option, g) : hostPassthrough(option, g);
471
+ const hosted = g.rebuilds.has(option) ? hostBuild(option, g, discriminator) : hostPassthrough(option, g, discriminator);
419
472
  if (hosted === null) return null;
420
473
  fn = hosted;
421
474
  hostedByOption.set(index, fn);
@@ -432,13 +485,13 @@ function buildDispatch(discriminator, cases, options, input, g) {
432
485
  * Host a non-rebuilding option as `value-or-FAIL`, so a union can probe it with
433
486
  * the same protocol as a rebuilding one.
434
487
  */
435
- function hostPassthrough(ir, g) {
488
+ function hostPassthrough(ir, g, discSkipKey) {
436
489
  const param = emitTemp(g.ctx, "bp");
437
490
  const scope = {
438
491
  temps: [],
439
492
  used: 0
440
493
  };
441
- const expr = generateFast(ir, createFastGen(param, g.ctx, true, scope));
494
+ const expr = generateFast(ir, createFastGen(param, g.ctx, true, scope, discSkipKey));
442
495
  if (expr === null) return null;
443
496
  const name = emitTemp(g.ctx, "vp");
444
497
  g.ctx.preamble.push(`function ${name}(${param}){${declareFastTemps(scope)}return ${expr === "true" ? param : `(${expr})?${param}:${g.fail}`};}`);
@@ -451,7 +504,7 @@ function hostPassthrough(ir, g) {
451
504
  * key is present, so the ordinary value is still returned by reference.
452
505
  */
453
506
  function passthrough(ir, input, g) {
454
- const expr = generateFast(ir, createFastGen(input, g.ctx, true, g.scope));
507
+ const expr = generateFast(ir, createFastGen(input, g.ctx, true, g.scope, g.discSkipKey));
455
508
  if (expr === null) return null;
456
509
  const guard = expr === "true" ? "" : `if(!(${expr}))return ${g.fail};`;
457
510
  if (!needsProtoScrub(ir)) return {
@@ -496,26 +549,41 @@ function buildObject(ir, input, g) {
496
549
  const nonoptional = new Set(ir.nonoptionalKeys ?? []);
497
550
  const refines = ir.checks ?? [];
498
551
  if (refines.some((check) => check.kind !== "refine_effect")) return null;
499
- let code = `if(typeof ${input}!=="object"||${input}===null||Array.isArray(${input}))return ${g.fail};`;
552
+ const skipKey = g.discSkipKey;
553
+ const childGen = skipKey === void 0 ? g : {
554
+ ...g,
555
+ discSkipKey: void 0
556
+ };
557
+ let code = skipKey === void 0 ? `if(typeof ${input}!=="object"||${input}===null||Array.isArray(${input}))return ${g.fail};` : "";
500
558
  if (ir.strict === true) {
501
559
  const keyVar = local(g, "bk");
502
560
  code += `for(${keyVar} in ${input}){if(!(${keyMembershipTest(g.ctx, Object.keys(ir.properties), keyVar)}))return ${g.fail};}`;
503
561
  }
504
- const slots = [];
505
- for (const [key, propIR] of parsedProperties(ir)) {
562
+ const properties = parsedProperties(ir);
563
+ const built = /* @__PURE__ */ new Map();
564
+ for (const [key, propIR] of orderedForBuild(properties, g.ctx)) {
506
565
  const keyStr = escapeString(key);
507
566
  const slot = local(g, "bv");
567
+ if (key === skipKey && !g.rebuilds.has(propIR)) {
568
+ code += `${slot}=${input}[${keyStr}];`;
569
+ built.set(key, {
570
+ code: "",
571
+ value: slot
572
+ });
573
+ continue;
574
+ }
508
575
  if (nonoptional.has(key)) code += `if(!(${keyStr} in ${input}))return ${g.fail};`;
509
576
  code += `${slot}=${input}[${keyStr}];`;
510
- const propBuilt = build(propIR, slot, g);
577
+ const propBuilt = build(propIR, slot, childGen);
511
578
  if (propBuilt === null) return null;
512
579
  code += propBuilt.code;
513
- slots.push({
514
- always: outputAlwaysDefined(propIR),
515
- keyStr,
516
- value: propBuilt.value
517
- });
580
+ built.set(key, propBuilt);
518
581
  }
582
+ const slots = properties.map(([key, propIR]) => ({
583
+ always: outputAlwaysDefined(propIR),
584
+ keyStr: escapeString(key),
585
+ value: built.get(key).value
586
+ }));
519
587
  const out = local(g, "bo");
520
588
  const literal = [];
521
589
  let appends = "";
@@ -750,15 +818,14 @@ function buildCoercedPrimitive(ir, input, g) {
750
818
  * `slowDefault` emits, reading the value off the retained schema so a
751
819
  * reference-typed default keeps zod's identity (one shared object, not a copy).
752
820
  *
753
- * The substituted value is not validated, so it makes the fast path a PARTIAL
754
- * predicate (`fc(undefined)` is false where the schema accepts) which is why
755
- * this records {@link CodeGenContext.buildSubstitutesValue}, on which
756
- * `generateValidator` withholds `.is()`.
821
+ * The substituted value is not validated. The by-reference fast form refuses
822
+ * `undefined` for that reason (it would hand the input back, not the default);
823
+ * the acceptance form `.is()` runs accepts it, as the schema does — see
824
+ * fastDefault.
757
825
  */
758
826
  function buildDefault(ir, input, g) {
759
827
  const inner = build(ir.inner, input, g);
760
828
  if (inner === null) return null;
761
- g.ctx.buildSubstitutesValue = true;
762
829
  const out = local(g, "bq");
763
830
  const value = defaultValueExpr(ir);
764
831
  const reapply = needsPostInnerDefault(ir) ? `if(${out}===undefined){${out}=${value};}` : "";