zod-compiler 1.28.0 → 2.0.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.
Files changed (92) hide show
  1. package/README.md +59 -53
  2. package/dist/core/codegen/build-path.d.ts +12 -1
  3. package/dist/core/codegen/build-path.d.ts.map +1 -1
  4. package/dist/core/codegen/build-path.js +47 -9
  5. package/dist/core/codegen/build-path.js.map +1 -1
  6. package/dist/core/codegen/context.d.ts +31 -16
  7. package/dist/core/codegen/context.d.ts.map +1 -1
  8. package/dist/core/codegen/context.js +43 -18
  9. package/dist/core/codegen/context.js.map +1 -1
  10. package/dist/core/codegen/emit-issue.d.ts +17 -20
  11. package/dist/core/codegen/emit-issue.d.ts.map +1 -1
  12. package/dist/core/codegen/emit-issue.js +13 -13
  13. package/dist/core/codegen/emit-issue.js.map +1 -1
  14. package/dist/core/codegen/index.d.ts.map +1 -1
  15. package/dist/core/codegen/index.js +9 -7
  16. package/dist/core/codegen/index.js.map +1 -1
  17. package/dist/core/codegen/issue-decls.d.ts +41 -7
  18. package/dist/core/codegen/issue-decls.d.ts.map +1 -1
  19. package/dist/core/codegen/issue-decls.js +49 -9
  20. package/dist/core/codegen/issue-decls.js.map +1 -1
  21. package/dist/core/codegen/schemas/array.js +1 -1
  22. package/dist/core/codegen/schemas/array.js.map +1 -1
  23. package/dist/core/codegen/schemas/bigint.d.ts.map +1 -1
  24. package/dist/core/codegen/schemas/bigint.js +15 -4
  25. package/dist/core/codegen/schemas/bigint.js.map +1 -1
  26. package/dist/core/codegen/schemas/catch.d.ts.map +1 -1
  27. package/dist/core/codegen/schemas/catch.js +3 -2
  28. package/dist/core/codegen/schemas/catch.js.map +1 -1
  29. package/dist/core/codegen/schemas/discriminated-union.d.ts.map +1 -1
  30. package/dist/core/codegen/schemas/discriminated-union.js +2 -1
  31. package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
  32. package/dist/core/codegen/schemas/effect.d.ts.map +1 -1
  33. package/dist/core/codegen/schemas/effect.js +4 -2
  34. package/dist/core/codegen/schemas/effect.js.map +1 -1
  35. package/dist/core/codegen/schemas/file.js +2 -8
  36. package/dist/core/codegen/schemas/file.js.map +1 -1
  37. package/dist/core/codegen/schemas/number.js +4 -3
  38. package/dist/core/codegen/schemas/number.js.map +1 -1
  39. package/dist/core/codegen/schemas/object.d.ts +11 -1
  40. package/dist/core/codegen/schemas/object.d.ts.map +1 -1
  41. package/dist/core/codegen/schemas/object.js +58 -14
  42. package/dist/core/codegen/schemas/object.js.map +1 -1
  43. package/dist/core/codegen/schemas/readonly.d.ts.map +1 -1
  44. package/dist/core/codegen/schemas/readonly.js.map +1 -1
  45. package/dist/core/codegen/schemas/record.d.ts.map +1 -1
  46. package/dist/core/codegen/schemas/record.js +8 -3
  47. package/dist/core/codegen/schemas/record.js.map +1 -1
  48. package/dist/core/codegen/schemas/sizeable.d.ts +49 -3
  49. package/dist/core/codegen/schemas/sizeable.d.ts.map +1 -1
  50. package/dist/core/codegen/schemas/sizeable.js +101 -8
  51. package/dist/core/codegen/schemas/sizeable.js.map +1 -1
  52. package/dist/core/codegen/schemas/string.d.ts.map +1 -1
  53. package/dist/core/codegen/schemas/string.js +45 -16
  54. package/dist/core/codegen/schemas/string.js.map +1 -1
  55. package/dist/core/codegen/schemas/tuple.d.ts +14 -11
  56. package/dist/core/codegen/schemas/tuple.d.ts.map +1 -1
  57. package/dist/core/codegen/schemas/tuple.js +99 -49
  58. package/dist/core/codegen/schemas/tuple.js.map +1 -1
  59. package/dist/core/codegen/well-known-regex.d.ts +7 -0
  60. package/dist/core/codegen/well-known-regex.d.ts.map +1 -1
  61. package/dist/core/codegen/well-known-regex.js +10 -3
  62. package/dist/core/codegen/well-known-regex.js.map +1 -1
  63. package/dist/core/extract/checks.d.ts.map +1 -1
  64. package/dist/core/extract/checks.js +1 -0
  65. package/dist/core/extract/checks.js.map +1 -1
  66. package/dist/core/extract/extractors/intersection.js +21 -6
  67. package/dist/core/extract/extractors/intersection.js.map +1 -1
  68. package/dist/core/extract/extractors/object.d.ts.map +1 -1
  69. package/dist/core/extract/extractors/object.js +25 -9
  70. package/dist/core/extract/extractors/object.js.map +1 -1
  71. package/dist/core/extract/extractors/optional.d.ts.map +1 -1
  72. package/dist/core/extract/extractors/optional.js +1 -28
  73. package/dist/core/extract/extractors/optional.js.map +1 -1
  74. package/dist/core/extract/extractors/record.js +9 -3
  75. package/dist/core/extract/extractors/record.js.map +1 -1
  76. package/dist/core/extract/extractors/tuple.d.ts.map +1 -1
  77. package/dist/core/extract/extractors/tuple.js +16 -20
  78. package/dist/core/extract/extractors/tuple.js.map +1 -1
  79. package/dist/core/extract/types.d.ts +16 -8
  80. package/dist/core/extract/types.d.ts.map +1 -1
  81. package/dist/core/iife.d.ts +2 -1
  82. package/dist/core/iife.d.ts.map +1 -1
  83. package/dist/core/iife.js +2 -1
  84. package/dist/core/iife.js.map +1 -1
  85. package/dist/core/types.d.ts +70 -21
  86. package/dist/core/types.d.ts.map +1 -1
  87. package/dist/jit.d.ts.map +1 -1
  88. package/dist/jit.js +47 -12
  89. package/dist/jit.js.map +1 -1
  90. package/dist/runtime.d.ts +3 -1
  91. package/dist/runtime.js +12 -10
  92. package/package.json +2 -2
package/README.md CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  **Compile Zod schemas into zero-overhead validation functions at build time.**
4
4
 
5
- Keep your existing Zod schemas. Get **1.2-43x faster** validation, and up to **190x** on rejected
5
+ Keep your existing Zod schemas. Get **1.1-46x faster** validation, and up to **41x** on rejected
6
6
  input. No code changes required.
7
7
 
8
+ Requires **Zod ≥ 4.5**. The compiled output reproduces 4.5's semantics exactly — code-point string
9
+ lengths, symbol-keyed shapes, tuple issue order — so it does not match earlier 4.x releases. Stay on
10
+ zod-compiler 1.x if you are on Zod 4.0–4.4.
11
+
8
12
  - [What Gets Compiled](#what-gets-compiled)
9
13
  - [Schema Hoisting](#schema-hoisting)
10
14
  - [Benchmark](#benchmark)
@@ -521,7 +525,7 @@ npx zod-compiler check src/schemas.ts --json --fail-under 80
521
525
 
522
526
  ## What Gets Compiled
523
527
 
524
- ### Fully Compiled (1.2-43x faster)
528
+ ### Fully Compiled (1.1-46x faster)
525
529
 
526
530
  Every Zod type except the fallbacks below — all primitives, `object` / `strictObject` / `looseObject`,
527
531
  `array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe`,
@@ -557,7 +561,7 @@ Three things differ by design:
557
561
 
558
562
  | Behavior | Zod | zod-compiler |
559
563
  | ------------------------- | ----------------------------------------------- | ------------------------------------------------------- |
560
- | Record key iteration | All own keys (`Reflect.ownKeys`) | Own enumerable **string** keys only |
564
+ | Record key iteration | Own enumerable keys, symbols included | Own enumerable **string** keys only |
561
565
  | Container output identity | A fresh array / set / map / object | The input container, by reference (array holes survive) |
562
566
  | Per-call parse params | `safeParse(x, { error, reportInput })` honoured | Ignored; global `z.config()` maps still apply |
563
567
 
@@ -572,59 +576,61 @@ Schema-level `error` and `z.config()` maps are unaffected; for a per-call map us
572
576
 
573
577
  | Scenario | Zod v3 | Zod v4 | **zod-compiler** | Typia | AJV | vs Zod v4 |
574
578
  | ----------------------------------------------- | ------ | ------ | ---------------- | ----- | ----- | --------- |
575
- | simple string | 12.6M | 14.3M | **16.5M** | 17.8M | 17.6M | 1.2x |
576
- | string (min/max) | 12.5M | 7.5M | **15.9M** | 17.0M | 15.2M | 2.1x |
577
- | number (int+positive) | 12.3M | 7.8M | **16.6M** | 16.8M | 17.3M | 2.1x |
578
- | enum | 11.7M | 12.1M | **16.2M** | 17.7M | 17.3M | 1.3x |
579
- | bigint (min/max) | 12.0M | 7.7M | **15.9M** | — | — | 2.1x |
580
- | tuple [string, int, bool] | 5.8M | 6.5M | **15.7M** | 17.1M | 16.3M | 2.4x |
581
- | record\<string, number\> | 3.2M | 2.8M | **15.2M** | 12.1M | 15.2M | 5.5x |
582
- | set\<string\> (5 items) | 3.7M | 2.3M | **14.7M** | — | — | 6.4x |
583
- | set\<string\> (20 items) | 1.3M | 683K | **12.0M** | — | — | **18x** |
584
- | map\<string, number\> (5 entries) | 2.1M | 1.4M | **13.1M** | — | — | 9.6x |
585
- | map\<string, number\> (20 entries) | 635K | 362K | **8.3M** | — | — | **23x** |
586
- | pipe (non-transform) | 8.6M | 5.6M | **15.9M** | — | — | 2.8x |
587
- | discriminatedUnion (3 variants) | 3.3M | 4.0M | **15.8M** | 15.5M | 7.7M | 4.0x |
588
- | discriminatedUnion (8 variants, rotating) | 2.7M | 3.4M | **9.2M** | — | — | 2.7x |
589
- | plain union of 8 tagged objects (auto-discrim.) | 363K | 632K | **9.1M** | — | — | **14x** |
590
- | strict object (DB row) | 1.8M | 3.1M | **10.9M** | — | — | 3.5x |
591
- | medium object (valid) | 1.9M | 2.4M | **9.7M** | 11.2M | 7.7M | 4.1x |
592
- | medium object (extra keys stripped) | 1.8M | 2.3M | **9.4M** | — | — | 4.2x |
593
- | medium object (invalid) | 504K | 80K | **14.7M** | 2.9M | 7.7M | **184x** |
594
- | large object (10 items) | 122K | 166K | **5.3M** | 5.9M | 1.2M | **32x** |
595
- | large object (100 items) | 13K | 18K | **781K** | 1.3M | 125K | **43x** |
596
- | readonly field (wrapper compiles away) | 3.1M | 4.4M | **15.7M** | — | — | 3.6x |
597
- | readonly root object (rebuild + freeze) | 2.9M | 3.8M | **12.2M** | — | — | 3.2x |
598
- | readonly array (delegates to Zod) | 3.9M | 2.9M | **2.9M** | — | — | 1.0x |
599
- | recursive tree (7 nodes) | 569K | 2.1M | **8.2M** | 11.6M | 4.8M | 3.9x |
600
- | recursive tree (121 nodes) | 32K | 135K | **800K** | 1.9M | 372K | 5.9x |
601
- | nested recursion (7 nodes) | 391K | 1.0M | **7.9M** | 11.1M | 3.1M | 7.8x |
602
- | nested recursion (121 nodes) | 24K | 62K | **818K** | 1.6M | 218K | **13x** |
603
- | deeply nested object (243 leaves) | 11K | 20K | **828K** | 1.1M | 117K | **42x** |
604
- | event log (combined) | 368K | 609K | **8.2M** | — | — | **13x** |
605
- | object with transform (zero-capture) | 1.2M | 1.9M | **6.5M** | — | — | 3.4x |
606
- | array 10 × transform (zero-capture) | 121K | 214K | **4.2M** | — | — | **20x** |
607
- | array 50 × transform (zero-capture) | 25K | 44K | **1.0M** | — | — | **24x** |
608
- | object with captured transform | 1.4M | 6.3M | **15.1M** | — | — | 2.4x |
609
- | object with captured refine (cross-field) | 1.6M | 2.4M | **15.3M** | — | — | 6.3x |
610
- | object with superRefine (cross-field) | 1.6M | 2.3M | **11.6M** | — | — | 5.0x |
611
- | coerced query object (valid) | 1.9M | 2.3M | **5.2M** | — | — | 2.2x |
612
- | coerced query object (invalid) | 1.1M | 162K | **10.1M** | — | — | **62x** |
613
- | preprocessed query object (valid) | 426K | 1.6M | **5.3M** | — | — | 3.4x |
614
- | preprocessed query object (invalid) | 387K | 149K | **12.4M** | — | — | **84x** |
615
- | stringbool config object (valid) | — | 1.9M | **6.0M** | — | — | 3.1x |
616
- | stringbool config object (invalid) | — | 129K | **13.1M** | — | — | **101x** |
617
- | custom/instanceof request (valid) | 894K | 3.0M | **8.6M** | — | — | 2.9x |
618
- | custom/instanceof request (invalid) | 755K | 158K | **8.5M** | — | — | **54x** |
619
- | disjoint object intersection (valid) | 1.4M | 1.7M | **9.7M** | — | — | 5.7x |
620
- | disjoint object intersection (invalid) | 548K | 80K | **15.3M** | — | — | **190x** |
621
-
622
- _ops/s, higher is better. `vp test bench` on an Apple M4 Max (zod 4.3.6, zod v3 3.23.8, typia 12, ajv 8),
623
- best of three runs. The harness costs ~55 ns per iteration, so the fastest rows sit at that floor and gaps
579
+ | simple string | 8.5M | 10.4M | **11.0M** | 11.1M | 11.2M | 1.1x |
580
+ | string (min/max) | 8.2M | 4.9M | **10.8M** | 11.0M | 10.0M | 2.2x |
581
+ | number (int+positive) | 7.9M | 6.7M | **10.7M** | 11.1M | 11.4M | 1.6x |
582
+ | enum | 8.0M | 9.4M | **11.1M** | 11.2M | 11.3M | 1.2x |
583
+ | bigint (min/max) | 7.6M | 5.6M | **10.9M** | — | — | 2.0x |
584
+ | tuple [string, int, bool] | 3.9M | 4.8M | **10.9M** | 10.6M | 10.1M | 2.3x |
585
+ | record\<string, number\> | 2.2M | 1.8M | **8.0M** | 7.3M | 9.6M | 4.6x |
586
+ | set\<string\> (5 items) | 2.5M | 1.5M | **9.9M** | — | — | 6.8x |
587
+ | set\<string\> (20 items) | 918K | 445K | **7.7M** | — | — | **17x** |
588
+ | map\<string, number\> (5 entries) | 1.4M | 864K | **8.7M** | — | — | **10x** |
589
+ | map\<string, number\> (20 entries) | 448K | 236K | **5.5M** | — | — | **24x** |
590
+ | pipe (non-transform) | 5.9M | 3.2M | **10.5M** | — | — | 3.3x |
591
+ | discriminatedUnion (3 variants) | 2.3M | 3.6M | **10.7M** | 10.3M | 5.4M | 3.0x |
592
+ | discriminatedUnion (8 variants, rotating) | 1.8M | 3.1M | **6.6M** | — | — | 2.1x |
593
+ | plain union of 8 tagged objects (auto-discrim.) | 244K | 898K | **6.1M** | — | — | 6.8x |
594
+ | strict object (DB row) | 1.2M | 2.0M | **7.0M** | — | — | 3.6x |
595
+ | medium object (valid) | 1.3M | 1.5M | **5.6M** | 6.9M | 4.7M | 3.9x |
596
+ | medium object (extra keys stripped) | 1.2M | 1.3M | **5.7M** | — | — | 4.3x |
597
+ | medium object (invalid) | 359K | 263K | **9.8M** | 2.0M | 5.1M | **37x** |
598
+ | large object (10 items) | 82K | 110K | **3.6M** | 3.8M | 765K | **33x** |
599
+ | large object (100 items) | 9K | 12K | **531K** | 759K | 82K | **46x** |
600
+ | readonly field (wrapper compiles away) | 2.2M | 4.7M | **11.0M** | — | — | 2.3x |
601
+ | readonly root object (rebuild + freeze) | 2.1M | 4.0M | **8.6M** | — | — | 2.1x |
602
+ | readonly array (delegates to Zod) | 2.8M | 3.0M | **2.9M** | — | — | 1.0x |
603
+ | recursive tree (7 nodes) | 407K | 727K | **5.3M** | 7.4M | 2.9M | 7.3x |
604
+ | recursive tree (121 nodes) | 23K | 42K | **510K** | 1.3M | 232K | **12x** |
605
+ | nested recursion (7 nodes) | 280K | 489K | **4.6M** | 7.2M | 1.8M | 9.3x |
606
+ | nested recursion (121 nodes) | 17K | 30K | **461K** | 1.1M | 126K | **15x** |
607
+ | deeply nested object (243 leaves) | 8K | 20K | **327K** | 666K | 83K | **17x** |
608
+ | event log (combined) | 257K | 575K | **4.9M** | — | — | 8.6x |
609
+ | object with transform (zero-capture) | 752K | 1.3M | **4.4M** | — | — | 3.4x |
610
+ | array 10 × transform (zero-capture) | 89K | 137K | **2.7M** | — | — | **20x** |
611
+ | array 50 × transform (zero-capture) | 18K | 28K | **689K** | — | — | **25x** |
612
+ | object with captured transform | 862K | 5.4M | **9.6M** | — | — | 1.8x |
613
+ | object with captured refine (cross-field) | 995K | 1.3M | **7.3M** | — | — | 5.6x |
614
+ | object with superRefine (cross-field) | 1.0M | 1.4M | **5.9M** | — | — | 4.3x |
615
+ | coerced query object (valid) | 1.2M | 2.0M | **3.6M** | — | — | 1.8x |
616
+ | coerced query object (invalid) | 754K | 603K | **6.8M** | — | — | **11x** |
617
+ | preprocessed query object (valid) | 302K | 1.1M | **3.6M** | — | — | 3.2x |
618
+ | preprocessed query object (invalid) | 268K | 574K | **7.8M** | — | — | **14x** |
619
+ | stringbool config object (valid) | — | 2.0M | **3.7M** | — | — | 1.9x |
620
+ | stringbool config object (invalid) | — | 497K | **8.6M** | — | — | **17x** |
621
+ | custom/instanceof request (valid) | 689K | 2.0M | **5.6M** | — | — | 2.7x |
622
+ | custom/instanceof request (invalid) | 527K | 624K | **5.7M** | — | — | 9.1x |
623
+ | disjoint object intersection (valid) | 946K | 1.0M | **5.7M** | — | — | 5.5x |
624
+ | disjoint object intersection (invalid) | 343K | 240K | **9.8M** | — | — | **41x** |
625
+
626
+ _ops/s, higher is better. `vp test bench` on an Apple M1 Max (zod 4.5.2, zod v3 3.23.8, typia 12, ajv 8),
627
+ best of three runs. The harness costs ~90 ns per iteration, so the fastest rows sit at that floor and gaps
624
628
  between the AOT columns there are noise, not real._
625
629
 
626
630
  Nested objects, arrays and recursive types gain the most. Rejection is fast because a failed
627
- `safeParse` defers building the error until `.error` is read.
631
+ `safeParse` defers building the error until `.error` is read. Zod 4.5 stopped capturing a stack trace on
632
+ that path too, so its own rejected-input rows are several times faster than 4.3's and the gap there is
633
+ narrower than it was.
628
634
 
629
635
  ```bash
630
636
  vp run benchmark # run locally
@@ -33,6 +33,17 @@ declare function rebuildsOutput(ir: SchemaIR): boolean;
33
33
  * size-gated `__fo_` split) to fix a shortcut that only the root takes.
34
34
  */
35
35
  declare function fastResultIsInput(ir: SchemaIR): boolean;
36
+ /**
37
+ * Does anything STRICTLY BELOW `ir` hand back a container needing a `__proto__`
38
+ * scrub?
39
+ *
40
+ * The root's by-reference shortcut can filter the value it returns
41
+ * (`data: __zcPs(input)`), but that only reaches the OUTER container — a nested
42
+ * one is never touched, because the shortcut does not walk. So a schema with a
43
+ * scrub-needing descendant keeps the eager slow walk, which scrubs at every
44
+ * level. Only the root's own scrub is shortcut-compatible.
45
+ */
46
+ declare function nestedNeedsProtoScrub(ir: SchemaIR): boolean;
36
47
  /**
37
48
  * Host the whole schema as `function NAME(input){…}` returning the built value
38
49
  * or the FAIL sentinel. Returns the function name, or null when the schema is
@@ -40,5 +51,5 @@ declare function fastResultIsInput(ir: SchemaIR): boolean;
40
51
  */
41
52
  declare function generateBuild(ir: SchemaIR, ctx: CodeGenContext): string | null;
42
53
  //#endregion
43
- export { fastResultIsInput, generateBuild, rebuildsOutput };
54
+ export { fastResultIsInput, generateBuild, nestedNeedsProtoScrub, rebuildsOutput };
44
55
  //# sourceMappingURL=build-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBA2JgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;iBA2GtB,cAAc,IAAI,UAAU,KAAK"}
1
+ {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBA8KgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
@@ -1,7 +1,8 @@
1
- import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, outputAlwaysDefined, rejectsUndefined } from "./context.js";
2
- import { ZC_HOP_DECL, ZC_PLAIN_DECL } from "./issue-decls.js";
1
+ import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput } from "./context.js";
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 { estimateFastCost, predictedInlineSize } from "./fast-size.js";
5
+ import { parsedProperties } from "./schemas/object.js";
5
6
  import { innerAppliesDefaultOnUndefined } from "./schemas/optional.js";
6
7
  import { fastStringCheck } from "./schemas/string.js";
7
8
  import { emitStringBoolMap, stringBoolUsesInline } from "./schemas/string-bool.js";
@@ -20,7 +21,7 @@ import { createFastGen, generateFast } from "./fast-path.js";
20
21
  * Resolving the ref against its target closes the cycle, and iterating to a
21
22
  * fixpoint settles the mutual dependency between the two.
22
23
  */
23
- function rebuildSet(root) {
24
+ function rebuildSet(root, includeProtoScrub = true) {
24
25
  const targets = /* @__PURE__ */ new Map([[0, root]]);
25
26
  const nodes = [];
26
27
  const seen = /* @__PURE__ */ new Set();
@@ -38,7 +39,7 @@ function rebuildSet(root) {
38
39
  for (const node of nodes) {
39
40
  if (rebuilds.has(node)) continue;
40
41
  const target = node.type === "recursiveRef" ? targets.get(node.refId ?? 0) : void 0;
41
- if (node.type === "object" && node.stripUnknownKeys === true || node.type === "readonly" && node.freeze === true || node.type === "default" || node.type === "stringBool" || node.type === "string" && (node.coerce === true || node.checks.some((c) => c.kind === "overwrite_effect")) || (node.type === "number" || node.type === "boolean" || node.type === "bigint" || node.type === "date") && node.coerce === true || node.type === "effect" || target !== void 0 && rebuilds.has(target) || children(node).some((child) => rebuilds.has(child))) {
42
+ if (node.type === "object" && node.stripUnknownKeys === true || node.type === "readonly" && node.freeze === true || node.type === "default" || node.type === "tuple" && tupleRewritesShortInput(node) || includeProtoScrub && needsProtoScrub(node) || node.type === "stringBool" || node.type === "string" && (node.coerce === true || node.checks.some((c) => c.kind === "overwrite_effect")) || (node.type === "number" || node.type === "boolean" || node.type === "bigint" || node.type === "date") && node.coerce === true || node.type === "effect" || target !== void 0 && rebuilds.has(target) || children(node).some((child) => rebuilds.has(child))) {
42
43
  rebuilds.add(node);
43
44
  changed = true;
44
45
  }
@@ -81,6 +82,7 @@ function rebuildsOutput(ir) {
81
82
  */
82
83
  function fastResultIsInput(ir) {
83
84
  if (rebuildsOutput(ir)) return false;
85
+ if (needsProtoScrub(ir)) return false;
84
86
  const seen = /* @__PURE__ */ new Set();
85
87
  const ordered = (node) => {
86
88
  if (seen.has(node)) return false;
@@ -122,6 +124,25 @@ function mutatesHere(ir) {
122
124
  function superRefines(checks) {
123
125
  return checks !== void 0 && checks.some((c) => c.kind === "super_refine_effect");
124
126
  }
127
+ /**
128
+ * Does anything STRICTLY BELOW `ir` hand back a container needing a `__proto__`
129
+ * scrub?
130
+ *
131
+ * The root's by-reference shortcut can filter the value it returns
132
+ * (`data: __zcPs(input)`), but that only reaches the OUTER container — a nested
133
+ * one is never touched, because the shortcut does not walk. So a schema with a
134
+ * scrub-needing descendant keeps the eager slow walk, which scrubs at every
135
+ * level. Only the root's own scrub is shortcut-compatible.
136
+ */
137
+ function nestedNeedsProtoScrub(ir) {
138
+ const seen = /* @__PURE__ */ new Set();
139
+ const walk = (node) => {
140
+ if (seen.has(node)) return false;
141
+ seen.add(node);
142
+ return children(node).some((child) => needsProtoScrub(child) || walk(child));
143
+ };
144
+ return walk(ir);
145
+ }
125
146
  function children(ir) {
126
147
  switch (ir.type) {
127
148
  case "object": return ir.catchall ? [...Object.values(ir.properties), ir.catchall] : Object.values(ir.properties);
@@ -153,6 +174,7 @@ function children(ir) {
153
174
  function generateBuild(ir, ctx) {
154
175
  const rebuilds = rebuildSet(ir);
155
176
  if (!rebuilds.has(ir) || mutatesBeyondStrip(ir)) return null;
177
+ if (needsProtoScrub(ir) && !rebuildSet(ir, false).has(ir)) return null;
156
178
  const fail = emitFailSentinel(ctx);
157
179
  const scope = {
158
180
  temps: [],
@@ -214,6 +236,7 @@ function build(ir, input, g) {
214
236
  };
215
237
  }
216
238
  }
239
+ if (needsProtoScrub(ir) && !children(ir).some((child) => g.rebuilds.has(child))) return passthrough(ir, input, g);
217
240
  const before = g.scope.used;
218
241
  const out = buildInline(ir, input, {
219
242
  ...g,
@@ -396,14 +419,25 @@ function hostPassthrough(ir, g) {
396
419
  g.ctx.preamble.push(`function ${name}(${param}){${declareFastTemps(scope)}return ${expr === "true" ? param : `(${expr})?${param}:${g.fail}`};}`);
397
420
  return name;
398
421
  }
399
- /** Validate in place with the Fast Path and hand the input straight back. */
422
+ /**
423
+ * Validate in place with the Fast Path and hand the input straight back —
424
+ * filtered through `__zcPs` for a container zod would have stripped an own
425
+ * `__proto__` from (see ZC_PROTO_SCRUB_DECL). The filter copies only when the
426
+ * key is present, so the ordinary value is still returned by reference.
427
+ */
400
428
  function passthrough(ir, input, g) {
401
429
  const expr = generateFast(ir, createFastGen(input, g.ctx, true, g.scope));
402
430
  if (expr === null) return null;
403
- return {
404
- code: expr === "true" ? "" : `if(!(${expr}))return ${g.fail};`,
431
+ const guard = expr === "true" ? "" : `if(!(${expr}))return ${g.fail};`;
432
+ if (!needsProtoScrub(ir)) return {
433
+ code: guard,
405
434
  value: input
406
435
  };
436
+ const slot = local(g, "bs");
437
+ return {
438
+ code: `${guard}${slot}=${emitRuntimeHelper(g.ctx, "__zcPs", ZC_PROTO_SCRUB_DECL)}(${input});`,
439
+ value: slot
440
+ };
407
441
  }
408
442
  /**
409
443
  * Rebuild from the declared keys. Sound for a stripping object (that IS the
@@ -432,7 +466,9 @@ function buildReadonly(ir, input, g) {
432
466
  function buildObject(ir, input, g) {
433
467
  if (ir.catchall !== void 0) return null;
434
468
  if (ir.stripUnknownKeys !== true && ir.strict !== true) return null;
469
+ if (ir.skipAbsentKeys !== void 0 && ir.skipAbsentKeys.length > 0) return null;
435
470
  if (ir.suppressAbsentKeys !== void 0 && ir.suppressAbsentKeys.length > 0) return null;
471
+ const nonoptional = new Set(ir.nonoptionalKeys ?? []);
436
472
  const refines = ir.checks ?? [];
437
473
  if (refines.some((check) => check.kind !== "refine_effect")) return null;
438
474
  let code = `if(typeof ${input}!=="object"||${input}===null||Array.isArray(${input}))return ${g.fail};`;
@@ -441,9 +477,10 @@ function buildObject(ir, input, g) {
441
477
  code += `for(${keyVar} in ${input}){if(!(${keyMembershipTest(g.ctx, Object.keys(ir.properties), keyVar)}))return ${g.fail};}`;
442
478
  }
443
479
  const slots = [];
444
- for (const [key, propIR] of Object.entries(ir.properties)) {
480
+ for (const [key, propIR] of parsedProperties(ir)) {
445
481
  const keyStr = escapeString(key);
446
482
  const slot = local(g, "bv");
483
+ if (nonoptional.has(key)) code += `if(!(${keyStr} in ${input}))return ${g.fail};`;
447
484
  code += `${slot}=${input}[${keyStr}];`;
448
485
  const propBuilt = build(propIR, slot, g);
449
486
  if (propBuilt === null) return null;
@@ -505,6 +542,7 @@ function buildArray(ir, input, g) {
505
542
  }
506
543
  function buildTuple(ir, input, g) {
507
544
  if (ir.rest !== null) return null;
545
+ if (ir.optStart !== ir.items.length) return null;
508
546
  if (ir.items.some((item) => !rejectsUndefined(item))) return null;
509
547
  let code = `if(!Array.isArray(${input})||${input}.length!==${ir.items.length})return ${g.fail};`;
510
548
  const values = [];
@@ -703,6 +741,6 @@ function local(g, prefix) {
703
741
  return name;
704
742
  }
705
743
  //#endregion
706
- export { fastResultIsInput, generateBuild, rebuildsOutput };
744
+ export { fastResultIsInput, generateBuild, nestedNeedsProtoScrub, rebuildsOutput };
707
745
 
708
746
  //# sourceMappingURL=build-path.js.map