zod-compiler 1.23.4 → 1.23.6
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 +27 -16
- package/dist/core/codegen/build-path.d.ts.map +1 -1
- package/dist/core/codegen/build-path.js +15 -5
- package/dist/core/codegen/build-path.js.map +1 -1
- package/dist/core/codegen/context.d.ts.map +1 -1
- package/dist/core/codegen/context.js +4 -2
- package/dist/core/codegen/context.js.map +1 -1
- package/dist/core/codegen/dedupe.js +3 -1
- package/dist/core/codegen/dedupe.js.map +1 -1
- package/dist/core/codegen/fast-path.d.ts.map +1 -1
- package/dist/core/codegen/fast-path.js +2 -0
- package/dist/core/codegen/fast-path.js.map +1 -1
- package/dist/core/codegen/fast-size.d.ts.map +1 -1
- package/dist/core/codegen/fast-size.js +2 -1
- package/dist/core/codegen/fast-size.js.map +1 -1
- package/dist/core/codegen/index.js +18 -1
- package/dist/core/codegen/index.js.map +1 -1
- package/dist/core/codegen/schemas/effect.d.ts +2 -2
- package/dist/core/codegen/schemas/effect.d.ts.map +1 -1
- package/dist/core/codegen/schemas/effect.js +12 -0
- package/dist/core/codegen/schemas/effect.js.map +1 -1
- package/dist/core/codegen/schemas/zod-delegate.d.ts +12 -0
- package/dist/core/codegen/schemas/zod-delegate.d.ts.map +1 -0
- package/dist/core/codegen/schemas/zod-delegate.js +14 -0
- package/dist/core/codegen/schemas/zod-delegate.js.map +1 -0
- package/dist/core/codegen/slow-path.d.ts.map +1 -1
- package/dist/core/codegen/slow-path.js +2 -0
- package/dist/core/codegen/slow-path.js.map +1 -1
- package/dist/core/diagnostic.d.ts.map +1 -1
- package/dist/core/diagnostic.js +3 -0
- package/dist/core/diagnostic.js.map +1 -1
- package/dist/core/extract/effects.d.ts +7 -1
- package/dist/core/extract/effects.d.ts.map +1 -1
- package/dist/core/extract/effects.js +24 -1
- package/dist/core/extract/effects.js.map +1 -1
- package/dist/core/extract/extractors/intersection.d.ts.map +1 -1
- package/dist/core/extract/extractors/intersection.js +32 -0
- package/dist/core/extract/extractors/intersection.js.map +1 -1
- package/dist/core/extract/extractors/pipe.d.ts.map +1 -1
- package/dist/core/extract/extractors/pipe.js +24 -1
- package/dist/core/extract/extractors/pipe.js.map +1 -1
- package/dist/core/types.d.ts +23 -2
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","names":[],"sources":["../../../src/core/codegen/context.ts"],"sourcesContent":["import type { BigIntCheckIR, CheckIR, DateCheckIR, SchemaIR, SetCheckIR } from \"../types.js\";\nimport type { SharedSchemaPlan } from \"./dedupe.js\";\nimport {\n fastTestSource,\n lookupWellKnownRegex,\n wellKnownRegexSourceName,\n} from \"./well-known-regex.js\";\n\n/** Codegen output mode. \"inline\" emits self-contained code (CLI .compiled.ts). \"lean\" emits references to imports from \"virtual:zod-compiler/runtime\" (unplugin). */\nexport type CodegenMode = \"inline\" | \"lean\";\n\nexport interface CodeGenResult {\n code: string;\n functionDef: string;\n /** Number of fallback schemas referenced by __rf[N] in the generated code. 0 = no fallbacks. */\n refCount: number;\n /**\n * Helper names referenced by this schema in lean mode (e.g. \"__zcTS\", \"__zcReEmail\").\n * Used by the unplugin transform to construct the `import { ... } from \"virtual:zod-compiler/runtime\"` line.\n * Always empty in inline mode.\n */\n usedHelpers: Set<string>;\n /**\n * Name of the hosted fast-check boolean function in the preamble (null when\n * the schema has no Fast Path). generateIIFE passes it to __zcMkv so parse()\n * and parseAsync() can return valid input without allocating an\n * intermediate SafeParseResult.\n */\n fastFnName: string | null;\n /**\n * True when `fastFnName` is a TOTAL predicate: `fc(input) === true` iff the\n * schema accepts `input` (mutation-free schemas, where a fast-check failure\n * can never become a slow-path success). generateIIFE installs it as the\n * zero-allocation `.is()` guard. False for partial fast paths\n * (default/catch — `fc` only shortcuts present-and-valid input, so a `false`\n * result does NOT imply rejection) and for schemas with no fast path, such as\n * coercion; `.is()` then derives from `safeParse(input).success`.\n */\n fastTotal: boolean;\n /**\n * Hosted predicate installed as `.is()`, when it differs from `fastFnName`.\n * A schema that rebuilds its output has no by-reference shortcut (so\n * `fastFnName` is null) yet still has an exact acceptance predicate, because\n * stripping reshapes the payload and never the verdict.\n */\n isFnName?: string | null;\n /**\n * Compact mode only: the `__rf[N]` index this validator delegates its cold\n * error path to (the schema itself, captured as a fresh root RefEntry). When\n * set, the pipeline appends a `{ schema, accessPath: \"\" }` entry at this index\n * so `generateIIFE` materializes `__rf[N]` as the original Zod schema. Absent\n * for every non-compact (fully compiled) validator.\n */\n rootDelegateRefIndex?: number;\n}\n\n/** Hosted-validator names for one recursion target (see CodeGenContext.recTargets). */\nexport interface RecTargetGen {\n /** True for the root target (refId 0) — reuses the schema's own functions. */\n isRoot: boolean;\n /**\n * safeParse-shaped slow validator name: `safeParse_<name>` for the root,\n * `__rsp_N` for a non-root target. `slowRecursiveRef` calls this.\n */\n slowName: string;\n /**\n * Boolean fast-check name (`__fcr_N`). Allocated lazily for the root (mirrors\n * recFastName), eagerly for non-root targets. Absent until the fast path\n * reaches a ref to this target.\n */\n fastName?: string;\n /** Inner IR hosted as the standalone validator body (non-root targets only). */\n inner?: SchemaIR;\n}\n\n/** Shared mutable state for code generation. Fast and slow paths share the same instance. */\nexport interface CodeGenContext {\n preamble: string[];\n counter: number;\n fnName: string;\n /** Deduplicates regex patterns: same pattern string → same preamble variable name. */\n regexCache: Map<string, string>;\n /** Codegen output mode. */\n mode: CodegenMode;\n /** Names of helpers from \"virtual:zod-compiler/runtime\" referenced in this schema (lean mode only). */\n usedHelpers: Set<string>;\n /**\n * Name of the fast-path boolean helper for the ROOT recursion target\n * (refId 0), allocated on first fastRecursiveRef visit. generateValidator\n * wraps the root fast expression as `function <name>(input){return <expr>;}`\n * so recursive refs can call it. undefined = root has no recursion on the\n * fast path.\n */\n recFastName?: string;\n /**\n * Hosted-validator name table for recursion targets, keyed by refId. Entry 0\n * is the root (the schema's own `safeParse_<name>` / `recFastName`); entries\n * ≥ 1 are non-root targets hosted as standalone `__rsp_N` (slow) / `__fcr_N`\n * (fast) helpers. `recursiveRef`/`recursionTarget` generators look up the\n * call target here. Undefined when not generating a full validator (e.g. unit\n * tests calling a single generator) — treated as root-only.\n */\n recTargets?: Map<number, RecTargetGen>;\n /** Dedup cache for hosted zero-capture effect functions: source text → preamble var. */\n effectFnCache?: Map<string, string>;\n /** Dedup cache for constant preamble declarations: initializer text → preamble var. */\n valueCache?: Map<string, string>;\n /** Name of the build path's FAIL sentinel, declared once per validator. */\n buildFailName?: string;\n /** Hosted build-function name per recursion target refId, so back-edges resolve. */\n buildRecNames?: Map<number, string>;\n /**\n * Set by the build path when it emitted a `.default()` substitution. Such a\n * schema ACCEPTS an input its fast expression rejects — `fastDefault` demands a\n * present value, since the fast path's contract is `data === input` and a\n * substituted default is not the input — so the expression is no longer an\n * exact acceptance predicate and must not be installed as `.is()`. Stripping,\n * by contrast, reshapes only the payload, which is why a build-path schema\n * otherwise still hands its predicate over.\n */\n buildSubstitutesValue?: boolean;\n /** Memo for estimateFastCost (size-gated fast-check extraction). Lazily created. */\n fastSizeCache?: WeakMap<SchemaIR, number>;\n /** Memo for estimateRuntimeCost (cheapest-first check ordering). Lazily created. */\n fastRuntimeCostCache?: WeakMap<SchemaIR, number>;\n /**\n * File-level shared slow-walk plan. Set only when generating a mutation-free\n * schema (so shared walks stay on the deferred cold path); the slow-path\n * visit() consults it to replace a repeated sub-IR with a `__zcSw_N` call.\n */\n sharedSchemas?: SharedSchemaPlan;\n}\n\n// ─── Slow Path context ────────────────────────────────────────────────────────\n\n/** Context object for slow-path (error-collecting) generator functions. */\nexport interface SlowGen {\n readonly input: string;\n readonly output: string;\n readonly path: string;\n readonly issues: string;\n readonly ctx: CodeGenContext;\n /**\n * Schema-level static error message of the node being generated\n * (z.string({ error: \"...\" })). Default message for issues this node emits\n * when the individual check has no message of its own. Set by generateSlow()\n * from ir.typeMessage; never inherited by child nodes.\n */\n readonly typeMsg?: string | undefined;\n\n /**\n * Name of a boolean variable that this node sets to `true` when it aborts in\n * the zod sense (`payload.aborted`) — currently only a pipe/codec whose `in`\n * step fails (zod's `handlePipeResult` sets `left.aborted = true`). A `union`\n * allocates one per option and reads it during pruning so a pipe option whose\n * `in` failed counts as aborted even when its only issue is a non-aborting\n * `custom`/check-level code. Undefined when the node is not inside an\n * abort-tracking option, in which case the abort is a no-op.\n *\n * Unlike input/output/path/issues, this is NOT inherited by `visit()`: it is\n * cleared at every boundary unless a node explicitly forwards it (the\n * pass-through wrappers optional/nullable/readonly do), mirroring how zod\n * propagates `payload.aborted` through transparent wrappers but not across\n * container boundaries.\n */\n readonly aborted?: string | undefined;\n\n /**\n * Recursively generate validation for a child IR node.\n * input/output/path/issues are inherited from parent unless overridden;\n * `aborted` is the exception — it is only set when explicitly passed (see the\n * `aborted` field doc), so it never leaks into container children.\n * Union generators use `{ issues }` to redirect child errors to temporary arrays.\n * Container generators use `{ input, output, path }` for element traversal.\n */\n visit(\n ir: SchemaIR,\n overrides?: {\n input?: string;\n output?: string;\n path?: string;\n issues?: string;\n // `| undefined` (unlike the others): pass-through wrappers forward\n // `g.aborted` verbatim, which is undefined outside an abort-tracking option.\n aborted?: string | undefined;\n },\n ): string;\n\n /** Generate a unique temp variable name: `__${prefix}_${counter++}` */\n temp(prefix: string): string;\n\n /** Add a regex to preamble and return the variable name. */\n regex(prefix: string, pattern: string, flags?: string): string;\n\n /** Add a Set to preamble and return the variable name. */\n set(prefix: string, values: readonly unknown[]): string;\n}\n\n/** Slow-path generator function signature — registered in slowRegistry. */\nexport type SlowGenerator<T extends SchemaIR = SchemaIR> = (ir: T, g: SlowGen) => string;\n\n// ─── Fast Path context ────────────────────────────────────────────────────────\n\n/**\n * Per-emitted-function size accumulator for size-gated fast-check extraction.\n * Shared by every node inlined into the same function; a fresh instance starts\n * each hosted helper (and the root). See fast-size.ts / generateFast.\n */\nexport interface FastScope {\n used: number;\n /**\n * `var` temps that the function this scope is assembling must declare, in\n * allocation order. Populated by {@link FastGen.local}; every site that\n * materializes a function body from a fresh scope emits\n * {@link declareFastTemps} at the top of that body.\n *\n * Function-scoped (never module-scoped) is load-bearing: a recursive\n * validator re-enters itself while an outer frame still holds a live temp,\n * and each invocation needs its own binding.\n */\n temps: string[];\n}\n\n/** `var a,b;` declaration for a scope's temps, or \"\" when it allocated none. */\nexport function declareFastTemps(scope: FastScope): string {\n return scope.temps.length > 0 ? `var ${scope.temps.join(\",\")};` : \"\";\n}\n\n/**\n * Fast check for a wrapper that compares its input against a sentinel and\n * otherwise delegates to an inner schema — `optional` (`===undefined`),\n * `nullable` (`===null`) and `default` (`!==undefined`).\n *\n * Written naively these read the input TWICE: once for the sentinel test and\n * again inside the inner check (which may itself read it several more times —\n * `typeof x===\"string\"&&x.length>=3&&x.length<=20`). V8's load elimination\n * removes the repeats only while the access is monomorphic; on the polymorphic\n * and megamorphic call sites real payloads produce (an array of objects with\n * differing key order, anything out of `JSON.parse`) every repeat is a fresh\n * megamorphic lookup. Binding the value to a local once is worth 1.1-1.7x on\n * the whole object check when the optional key is present, and is neutral when\n * it is absent.\n *\n * Only hoisted when the input is a property access; a bare local (an array\n * element variable, a record value) is already a single load, so it keeps the\n * shorter form and byte-identical output.\n */\nexport function fastSentinelWrapper(\n g: FastGen,\n innerIR: SchemaIR,\n sentinel: string,\n joiner: \"&&\" | \"||\",\n): string | null {\n if (!isPropertyAccess(g.input)) {\n const inner = g.visit(innerIR);\n return inner === null ? null : `(${g.input}${sentinel}${joiner}(${inner}))`;\n }\n const value = g.local(\"w\");\n const inner = g.visit(innerIR, { input: value });\n if (inner === null) return null;\n return `((${value}=${g.input})${sentinel}${joiner}(${inner}))`;\n}\n\n/** True for an expression that performs a property load (`x[\"a\"]`, `x.a`, `x[0][1]`). */\nfunction isPropertyAccess(expr: string): boolean {\n return expr.includes(\"[\") || expr.includes(\".\");\n}\n\n/** Context object for fast-path (boolean expression) generator functions. */\nexport interface FastGen {\n readonly input: string;\n readonly ctx: CodeGenContext;\n\n /**\n * Whether the CURRENT node may be hoisted into its own boolean helper when it\n * (with the already-emitted siblings) would overflow the function size cap.\n * False for the root and for a helper's own top node — those are already their\n * own function — but their children are extractable. See generateFast.\n */\n readonly extractable: boolean;\n\n /** Accumulated size (≈ chars) of the function currently being assembled. */\n readonly scope: FastScope;\n\n /**\n * Set on the gen for a discriminated-union option only: the discriminator\n * key. Signals `fastObject` to omit its type-guard and skip re-checking that\n * property (the switch already matched its value). Never propagated to child\n * nodes — nested objects keep their own guard.\n */\n readonly discSkipKey?: string | undefined;\n\n /**\n * Recursively generate fast-check expression for a child IR node.\n * Returns null if any child is ineligible for fast path.\n */\n visit(ir: SchemaIR, overrides?: { input?: string; discSkipKey?: string }): string | null;\n\n /**\n * A FastGen for emitting a SEPARATE function body (a hand-built preamble\n * helper such as a discriminated-union switch or an array-element loop). It\n * carries a FRESH size accumulator, so the helper's own content is size-gated\n * against the cap independently of the caller — without this, a helper's body\n * accrues to the caller's scope while the helper itself grows unbounded.\n */\n scoped(input: string): FastGen;\n\n /** Generate a unique temp variable name. */\n temp(prefix: string): string;\n\n /**\n * Allocate a unique name AND record it on this scope so the enclosing\n * emitted function declares it as a `var` (see {@link FastScope.temps}).\n * Use for a value bound inside an expression — `(t=x[\"k\"])===undefined` —\n * where `temp()` alone would leave the name undeclared.\n */\n local(prefix: string): string;\n\n /** Add a regex to preamble and return the variable name. */\n regex(prefix: string, pattern: string, flags?: string): string;\n}\n\n/** Fast-path generator function signature — registered in fastRegistry. */\nexport type FastGenerator<T extends SchemaIR = SchemaIR> = (ir: T, g: FastGen) => string | null;\n\n// ─── Shared emit helpers (used by both slow-path and fast-path factories) ────\n\n/** Allocate a fresh `__${prefix}_${n}` identifier and bump the shared counter. */\nexport function emitTemp(ctx: CodeGenContext, prefix: string): string {\n return `__${prefix}_${ctx.counter++}`;\n}\n\n/**\n * Host a zero-capture effect function (refine predicate, transform,\n * overwrite) in the preamble and return its variable name. The inline\n * `(${source})(x)` form evaluates the function expression — allocating a\n * function object — on EVERY parse at every effect site, including inside\n * the \"zero-allocation\" fast chain. V8's escape analysis erases that in\n * optimized frames, but interpreter/baseline/deopt frames pay it, and the\n * full source text re-parses as bytecode at each site. Zero-capture sources\n * reference only their own parameters and safe globals by construction, so\n * a single preamble binding is semantically identical. Deduped per schema\n * by source text.\n */\nexport function emitEffectFn(ctx: CodeGenContext, source: string): string {\n ctx.effectFnCache ??= new Map();\n const cached = ctx.effectFnCache.get(source);\n if (cached !== undefined) return cached;\n const name = `__ef_${ctx.counter++}`;\n ctx.preamble.push(`var ${name}=(${source});`);\n ctx.effectFnCache.set(source, name);\n return name;\n}\n\n/**\n * Callable expression for a user callback — a refine predicate or a transform.\n *\n * A zero-capture callback is hosted from its source text; one that CAPTURES\n * outer variables is called by reference through `__rf[N]` — the user's own\n * function object, reached from the schema — instead of costing the schema its\n * compiled path. The reference is aliased into a preamble binding rather than\n * re-read per call, for the same reason call-invoked helpers are (a per-call\n * array element load is not a foldable callee).\n */\nexport function emitEffectCallable(\n ctx: CodeGenContext,\n effect: { refIndex?: number | undefined; source?: string | undefined },\n): string {\n if (effect.refIndex !== undefined) return emitConstant(ctx, \"rfn\", `__rf[${effect.refIndex}]`);\n if (effect.source === undefined) {\n throw new Error(\"effect has neither inlineable source nor a reference index\");\n }\n return emitEffectFn(ctx, effect.source);\n}\n\n/**\n * Pristine fallback delegate: declare `var __rfp_N=__rf[N].safeParse.bind(__rf[N]);`\n * in the preamble and return the variable name. Generated code must NEVER read\n * `__rf[N].safeParse` at parse time: `__zcMkv` installs the compiled safeParse as\n * an OWN property on the original schema object, and whenever `__rf[N]` is that\n * same object the read resolves to the compiled delegate itself — infinite\n * recursion (RangeError on every parse). The fallback entry and the __zcMkv\n * target ARE the same object in compile mode (schemaExpr is the compile()\n * argument identifier) and the CLI emitter ((__src_X as any).schema); in\n * autoDiscover mode they are two textually identical constructions that any\n * downstream CSE/dedup transform (babel-plugin-zod-hoist in a field incident)\n * collapses back into one. Capturing at IIFE evaluation — before the trailing\n * `return __zcMkv(...)` mutates anything — pins zod's own implementation; the\n * worst case under cross-validator merges is delegating to an equivalent\n * compiled validator (whose own delegates were captured even earlier), never\n * a cycle.\n */\nexport function emitRfDelegate(ctx: CodeGenContext, refIndex: number): string {\n const name = `__rfp_${refIndex}`;\n const decl = `var ${name}=__rf[${refIndex}].safeParse.bind(__rf[${refIndex}]);`;\n if (!ctx.preamble.includes(decl)) {\n ctx.preamble.push(decl);\n }\n return name;\n}\n\n/**\n * Resolve a regex pattern to a runtime variable name.\n * Lean mode short-circuits well-known patterns to virtual-module names so the\n * bundler can dedup across files; everything else is cached + declared in the\n * per-IIFE preamble exactly once per pattern.\n */\nexport function emitRegex(\n ctx: CodeGenContext,\n prefix: string,\n pattern: string,\n flags?: string,\n): string {\n if (ctx.mode === \"lean\" && !flags) {\n const wellKnown = lookupWellKnownRegex(pattern);\n if (wellKnown !== null) {\n ctx.usedHelpers.add(wellKnown);\n return wellKnown;\n }\n }\n const cacheKey = flags ? `${flags}\\u0000${pattern}` : pattern;\n const cached = ctx.regexCache.get(cacheKey);\n if (cached) return cached;\n const name = `__re_${prefix}_${ctx.counter++}`;\n const flagsArg = flags ? `,${escapeString(flags)}` : \"\";\n // Flag-less patterns may carry a faster behavior-equivalent rewrite (a\n // well-known table entry, repeat unrolling, or both); the regex OBJECT uses\n // it while issue sites keep reporting the original pattern (see slowString).\n const testSource = flags ? null : fastTestSource(pattern);\n ctx.preamble.push(`var ${name}=new RegExp(${escapeString(testSource ?? pattern)}${flagsArg});`);\n ctx.regexCache.set(cacheKey, name);\n return name;\n}\n\n/**\n * Resolve the ORIGINAL `/source/flags` pattern string of a regex for issue\n * reporting. Only needed when emitRegex swapped in a faster equivalent test\n * pattern (the runtime regex's toString() would leak the rewrite). Lean mode\n * references the shared `<name>Src` virtual export so the original pattern\n * stays a single bundle-wide string; inline mode declares it once per IIFE.\n */\nexport function emitRegexSourceString(ctx: CodeGenContext, pattern: string): string {\n if (ctx.mode === \"lean\") {\n const srcName = wellKnownRegexSourceName(pattern);\n if (srcName !== null) {\n ctx.usedHelpers.add(srcName);\n return srcName;\n }\n }\n const cacheKey = `src\\u0000${pattern}`;\n const cached = ctx.regexCache.get(cacheKey);\n if (cached) return cached;\n const name = `__res_${ctx.counter++}`;\n ctx.preamble.push(`var ${name}=${escapeString(`/${pattern}/`)};`);\n ctx.regexCache.set(cacheKey, name);\n return name;\n}\n\n/**\n * Declare a constant value in the preamble and return its variable name,\n * reusing an earlier declaration of the SAME initializer.\n *\n * Value tables are reached from both halves of a validator — an enum's `Set`\n * from its fast check and again from its slow walk, a strict shape's key table\n * likewise — and repeat across sibling properties that share a value list. One\n * declaration per USE emitted the payload two or four times: measured 17% of a\n * 20-value enum schema's generated bytes, 16% for an object with two identical\n * enums. Keyed by initializer text, so only identical payloads collapse.\n */\nexport function emitConstant(ctx: CodeGenContext, prefix: string, initializer: string): string {\n ctx.valueCache ??= new Map();\n const cached = ctx.valueCache.get(initializer);\n if (cached !== undefined) return cached;\n const name = `__${prefix}_${ctx.counter++}`;\n ctx.preamble.push(`var ${name}=${initializer};`);\n ctx.valueCache.set(initializer, name);\n return name;\n}\n\n/** Declare a `new Set([...])` in the preamble and return its variable name. */\nexport function emitSet(ctx: CodeGenContext, prefix: string, values: readonly unknown[]): string {\n return emitConstant(ctx, `set_${prefix}`, `new Set(${JSON.stringify([...values])})`);\n}\n\n/**\n * Shape-key count at or below which the unknown-key pass compares with an\n * inline `===` chain rather than a hashed lookup.\n *\n * This is deliberately NOT {@link ENUM_INLINE_THRESHOLD}: the two look alike but\n * are different workloads. An enum compares schema literals against arbitrary\n * INPUT strings, which may be long, share prefixes, and are not necessarily\n * internalized — so a hashed set earns its keep quickly. A shape-key test\n * compares them against keys arriving from `for-in`, i.e. the object's own\n * internalized key strings, so every arm of the chain is a pointer compare that\n * V8 predicts perfectly, while `table[k]` / `set.has(k)` pays a string hash and\n * probe per key.\n *\n * Measured over a strict object's for-in pass (JSON-parsed input, 8 rotated\n * shapes), `===` chain vs the previous `{k:1}` table: 3.7x at 6 keys, 3.8x at\n * 10, 3.1x at 20, 3.1x at 48 — the chain still leads at 64 (283 ns vs 690) and\n * only loses past ~96, where `Set.has` (not the table, which never wins at any\n * size) takes over. 64 sits below that crossover and above any realistic shape.\n */\nexport const KEY_MEMBERSHIP_INLINE_THRESHOLD = 64;\n\n/**\n * Boolean membership test for one key variable against a fixed key list.\n * Empty list recognizes nothing.\n *\n * `Set.has` is the large-shape fallback rather than a `{key:1}` object table:\n * the table is also `__proto__`-hostile (an own `__proto__` key cannot be set\n * by an object literal, so that key would silently read as unknown), which the\n * Set has no trouble with.\n */\nexport function keyMembershipTest(\n ctx: CodeGenContext,\n keys: readonly string[],\n keyVar: string,\n): string {\n if (keys.length === 0) return \"false\";\n if (keys.length <= KEY_MEMBERSHIP_INLINE_THRESHOLD) {\n return keys.map((k) => `${keyVar}===${escapeString(k)}`).join(\"||\");\n }\n return `${emitSet(ctx, \"ks\", keys)}.has(${keyVar})`;\n}\n\n/**\n * Enum values at or below this count use inline === checks instead of Set.has().\n * Measured on V8: for ≤5 values, an === chain beats Set.has by up to ~3x with\n * realistic (distinct-prefix, JSON-parsed) values — V8 internalizes strings on\n * successful comparison, making subsequent arms pointer-equality — and is no\n * worse than Set.has even with adversarial shared-prefix values.\n */\nexport const ENUM_INLINE_THRESHOLD = 5;\n\nconst CHECK_PRIORITY: Record<string, number> = {\n // Cheapest: length/size comparisons (O(1))\n min_length: 10,\n max_length: 11,\n length_equals: 12,\n min_size: 13,\n max_size: 14,\n // Number format checks (comparison + bitwise)\n number_format: 15,\n // Range comparisons\n greater_than: 20,\n less_than: 21,\n bigint_greater_than: 20,\n bigint_less_than: 21,\n date_greater_than: 22,\n date_less_than: 23,\n // Modulo\n multiple_of: 30,\n bigint_multiple_of: 30,\n // String prefix/suffix (O(prefix/suffix length))\n starts_with: 40,\n ends_with: 41,\n // String search (O(n·m) worst case)\n includes: 42,\n // Regex (most expensive)\n string_format: 50,\n};\n\nexport function escapeString(s: string | number): string {\n return JSON.stringify(s);\n}\n\n/**\n * JS source for a primitive literal value (literal schemas, discriminator\n * case labels). JSON.stringify covers string/number/boolean/null; bigint\n * needs the `n` suffix (JSON.stringify throws and String(5n) renders a\n * number literal that never strict-equals a bigint); undefined isn't JSON.\n */\nexport function literalToJs(v: string | number | boolean | null | bigint | undefined): string {\n if (typeof v === \"bigint\") return `${v}n`;\n if (v === undefined) return \"undefined\";\n // JSON.stringify maps NaN/±Infinity to \"null\"; emit them as JS expressions so a\n // non-finite numeric literal round-trips (z.literal(Infinity) must compare\n // against Infinity, not null). String(NaN)=\"NaN\", String(Infinity)=\"Infinity\",\n // String(-Infinity)=\"-Infinity\" — all valid JS that evaluate to the value.\n if (typeof v === \"number\" && !Number.isFinite(v)) return String(v);\n return JSON.stringify(v);\n}\n\n/**\n * Helpers that generated code invokes through `Function.prototype.call`, and\n * which therefore must be aliased into a module-local binding in lean mode.\n *\n * V8 folds a local `const` callee into a constant and inlines straight through\n * `x.call(...)`; an IMPORTED binding is a cell it will not fold, so the same\n * expression stays a generic property load plus a generic call — measured 4.5x\n * (5 keys) to 6.5x (20 keys) slower on the record fast path, 35.9 ns vs 7.2 ns\n * for a 5-key record. Aliasing the import into the IIFE recovers all of it\n * (7.4 ns). A DIRECT call to an imported function (`__zcFsr(v,s)`) is not\n * penalized — measured identical — and neither is an imported RegExp receiver,\n * so only the `.call` sites are listed here.\n */\nconst CALL_INVOKED_HELPERS: ReadonlySet<string> = new Set([\"__zcHop\"]);\n\n/**\n * Reference a shared runtime helper (e.g. __zcFsr) from generated code.\n * Lean mode: registers it for the `virtual:zod-compiler/runtime` import.\n * Inline mode: declares it once in the per-IIFE preamble.\n */\nexport function emitRuntimeHelper(ctx: CodeGenContext, name: string, decl: string): string {\n if (ctx.mode === \"lean\") {\n ctx.usedHelpers.add(name);\n if (CALL_INVOKED_HELPERS.has(name)) return emitConstant(ctx, \"lh\", name);\n } else if (!ctx.preamble.includes(decl)) {\n ctx.preamble.push(decl);\n }\n return name;\n}\n\n/**\n * Extend a path expression with one or more scalar segment expressions\n * (escaped string literals, numeric literals, or loop-variable names).\n *\n * Path expressions are only ever composed by these helpers starting from the\n * `[]` root, so any path that looks like an array literal IS one — the new\n * segment is spliced in to keep issue paths a single array allocation\n * (`[\"data\",\"items\",__i_7]`) instead of an allocation per nesting level\n * (`[\"data\"].concat(\"items\").concat(__i_7)`). Opaque expressions fall back\n * to .concat().\n */\nexport function extendPath(parentPath: string, segExpr: string): string {\n if (parentPath === \"[]\") return `[${segExpr}]`;\n if (parentPath.startsWith(\"[\") && parentPath.endsWith(\"]\")) {\n return `${parentPath.slice(0, -1)},${segExpr}]`;\n }\n return `${parentPath}.concat(${segExpr})`;\n}\n\n/** Extend a path expression with a static string key. */\nexport function extendStaticPath(parentPath: string, key: string): string {\n return extendPath(parentPath, escapeString(key));\n}\n\n/** Extend a path expression with a numeric index. */\nexport function extendStaticPathIndex(parentPath: string, index: number): string {\n return extendPath(parentPath, String(index));\n}\n\n/**\n * A superRefine callback receives zod's payload, whose `value` is public,\n * typed, writable API ($RefinementCtx extends ParsePayload) — so any node\n * carrying one MAY rewrite its value and must be treated as mutating. Which\n * callbacks actually do is undecidable here; the emitted fast check settles it\n * at runtime by refusing when the value changed (see ZC_SR_OK_DECL), so a\n * non-mutating callback still exits through the fast path.\n */\nfunction hasSuperRefine(checks: readonly { kind: string }[] | undefined): boolean {\n return checks !== undefined && checks.some((c) => c.kind === \"super_refine_effect\");\n}\n\n/**\n * Check if a SchemaIR tree produces output that is not the input itself —\n * either value-mutating operations (coerce, default, catch, overwrite) that\n * write back to the input expression, or a strip object that rebuilds a fresh\n * object from its known keys. Used by container generators to decide whether to\n * clone (so the rebuilt/mutated value never writes through to the caller's\n * input), by generateValidator to keep such schemas off the by-reference fast\n * path, and by the shared-walk dedup + intersection extractor to exclude them.\n */\nexport function hasMutation(ir: SchemaIR): boolean {\n switch (ir.type) {\n case \"string\":\n // url checks trim (and optionally normalize) the value; overwrite\n // effects (.trim(), .toLowerCase()) rewrite it.\n return (\n ir.coerce === true ||\n hasSuperRefine(ir.checks) ||\n ir.checks.some(\n (c) =>\n c.kind === \"overwrite_effect\" || (c.kind === \"string_format\" && c.format === \"url\"),\n )\n );\n case \"number\":\n return ir.coerce === true || hasSuperRefine(ir.checks);\n case \"boolean\":\n case \"bigint\":\n case \"date\":\n return ir.coerce === true;\n case \"default\":\n case \"catch\":\n case \"effect\":\n case \"fallback\":\n case \"stringBool\":\n return true;\n case \"object\":\n // A strip object produces a FRESH output (only the declared keys), so it\n // mutates: parents must clone before it writes back, it never takes the\n // by-reference fast path, and intersections of strip objects delegate to\n // zod (see extractIntersection's hasMutation guard) — matching zod's\n // parse-both-sides-then-merge semantics instead of over-stripping.\n return (\n ir.stripUnknownKeys === true ||\n hasSuperRefine(ir.checks) ||\n (ir.catchall !== undefined && hasMutation(ir.catchall)) ||\n Object.values(ir.properties).some((p) => hasMutation(p))\n );\n case \"array\":\n return hasSuperRefine(ir.checks) || hasMutation(ir.element);\n case \"tuple\":\n return ir.items.some(hasMutation) || (ir.rest !== null && hasMutation(ir.rest));\n case \"record\":\n return hasMutation(ir.valueType);\n case \"optional\":\n case \"nullable\":\n case \"readonly\":\n case \"recursionTarget\":\n return hasMutation(ir.inner);\n case \"union\":\n case \"discriminatedUnion\":\n return ir.options.some(hasMutation);\n case \"intersection\":\n return hasMutation(ir.left) || hasMutation(ir.right);\n case \"pipe\":\n return hasMutation(ir.in) || hasMutation(ir.out);\n case \"set\":\n return hasMutation(ir.valueType);\n case \"map\":\n return hasMutation(ir.keyType) || hasMutation(ir.valueType);\n case \"file\":\n return false;\n default:\n return false;\n }\n}\n\n/**\n * Is a defaulted property's key guaranteed to appear in the stripped output?\n *\n * Distinct from {@link rejectsUndefined}, which asks whether `undefined` is\n * REJECTED — a `.default()` accepts it and yet still produces a defined value, so\n * only this question earns the key a slot in the output object literal. The two\n * answers coincide everywhere else.\n *\n * Sound for both branches of a default: the substituted value is defined\n * (`alwaysDefined`, checked against the schema at extraction time), and the inner\n * branch runs only when `input[key] !== undefined`, which implies `key in input`\n * — so zod's presence test keeps the key whatever the inner produced.\n */\nexport function outputAlwaysDefined(ir: SchemaIR): boolean {\n return ir.type === \"default\" ? ir.alwaysDefined === true : rejectsUndefined(ir);\n}\n\n/**\n * Does this schema reject `undefined` outright?\n *\n * Read as \"can this slot be ABSENT from the input\" by the tuple build, whose\n * output length depends on it — zod marks a defaulted or optional item\n * `optin: \"optional\"` and accepts a shorter array. Conservative: anything that\n * might accept, produce, or default to `undefined` answers false.\n */\nexport function rejectsUndefined(ir: SchemaIR): boolean {\n switch (ir.type) {\n // Coercion turns undefined into a value (`String(undefined)`), so a\n // coercing primitive is NOT a rejector.\n case \"string\":\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n case \"date\":\n return ir.coerce !== true;\n case \"symbol\":\n case \"null\":\n case \"nan\":\n case \"never\":\n case \"enum\":\n case \"object\":\n case \"array\":\n case \"tuple\":\n case \"record\":\n case \"set\":\n case \"map\":\n case \"file\":\n case \"templateLiteral\":\n case \"discriminatedUnion\":\n case \"stringBool\":\n return true;\n case \"literal\":\n return !ir.values.includes(undefined);\n case \"union\":\n return ir.options.every(rejectsUndefined);\n case \"intersection\":\n return rejectsUndefined(ir.left) || rejectsUndefined(ir.right);\n case \"nullable\":\n case \"readonly\":\n case \"recursionTarget\":\n return rejectsUndefined(ir.inner);\n default:\n // optional / any / unknown / undefined / void / default / catch /\n // fallback / effect / pipe / recursiveRef — each can yield undefined,\n // or is opaque enough that we must not assume otherwise.\n return false;\n }\n}\n\n/**\n * Sort comparator for CheckIR: cheapest/most-discriminating checks first.\n * Used by fast-path generators after filtering out refine_effect entries.\n */\nexport function checkPriority(\n a: CheckIR | BigIntCheckIR | DateCheckIR | SetCheckIR,\n b: CheckIR | BigIntCheckIR | DateCheckIR | SetCheckIR,\n): number {\n return (CHECK_PRIORITY[a.kind] ?? 99) - (CHECK_PRIORITY[b.kind] ?? 99);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../../src/core/codegen/context.ts"],"sourcesContent":["import type { BigIntCheckIR, CheckIR, DateCheckIR, SchemaIR, SetCheckIR } from \"../types.js\";\nimport type { SharedSchemaPlan } from \"./dedupe.js\";\nimport {\n fastTestSource,\n lookupWellKnownRegex,\n wellKnownRegexSourceName,\n} from \"./well-known-regex.js\";\n\n/** Codegen output mode. \"inline\" emits self-contained code (CLI .compiled.ts). \"lean\" emits references to imports from \"virtual:zod-compiler/runtime\" (unplugin). */\nexport type CodegenMode = \"inline\" | \"lean\";\n\nexport interface CodeGenResult {\n code: string;\n functionDef: string;\n /** Number of fallback schemas referenced by __rf[N] in the generated code. 0 = no fallbacks. */\n refCount: number;\n /**\n * Helper names referenced by this schema in lean mode (e.g. \"__zcTS\", \"__zcReEmail\").\n * Used by the unplugin transform to construct the `import { ... } from \"virtual:zod-compiler/runtime\"` line.\n * Always empty in inline mode.\n */\n usedHelpers: Set<string>;\n /**\n * Name of the hosted fast-check boolean function in the preamble (null when\n * the schema has no Fast Path). generateIIFE passes it to __zcMkv so parse()\n * and parseAsync() can return valid input without allocating an\n * intermediate SafeParseResult.\n */\n fastFnName: string | null;\n /**\n * True when `fastFnName` is a TOTAL predicate: `fc(input) === true` iff the\n * schema accepts `input` (mutation-free schemas, where a fast-check failure\n * can never become a slow-path success). generateIIFE installs it as the\n * zero-allocation `.is()` guard. False for partial fast paths\n * (default/catch — `fc` only shortcuts present-and-valid input, so a `false`\n * result does NOT imply rejection) and for schemas with no fast path, such as\n * coercion; `.is()` then derives from `safeParse(input).success`.\n */\n fastTotal: boolean;\n /**\n * Hosted predicate installed as `.is()`, when it differs from `fastFnName`.\n * A schema that rebuilds its output has no by-reference shortcut (so\n * `fastFnName` is null) yet still has an exact acceptance predicate, because\n * stripping reshapes the payload and never the verdict.\n */\n isFnName?: string | null;\n /**\n * Compact mode only: the `__rf[N]` index this validator delegates its cold\n * error path to (the schema itself, captured as a fresh root RefEntry). When\n * set, the pipeline appends a `{ schema, accessPath: \"\" }` entry at this index\n * so `generateIIFE` materializes `__rf[N]` as the original Zod schema. Absent\n * for every non-compact (fully compiled) validator.\n */\n rootDelegateRefIndex?: number;\n}\n\n/** Hosted-validator names for one recursion target (see CodeGenContext.recTargets). */\nexport interface RecTargetGen {\n /** True for the root target (refId 0) — reuses the schema's own functions. */\n isRoot: boolean;\n /**\n * safeParse-shaped slow validator name: `safeParse_<name>` for the root,\n * `__rsp_N` for a non-root target. `slowRecursiveRef` calls this.\n */\n slowName: string;\n /**\n * Boolean fast-check name (`__fcr_N`). Allocated lazily for the root (mirrors\n * recFastName), eagerly for non-root targets. Absent until the fast path\n * reaches a ref to this target.\n */\n fastName?: string;\n /** Inner IR hosted as the standalone validator body (non-root targets only). */\n inner?: SchemaIR;\n}\n\n/** Shared mutable state for code generation. Fast and slow paths share the same instance. */\nexport interface CodeGenContext {\n preamble: string[];\n counter: number;\n fnName: string;\n /** Deduplicates regex patterns: same pattern string → same preamble variable name. */\n regexCache: Map<string, string>;\n /** Codegen output mode. */\n mode: CodegenMode;\n /** Names of helpers from \"virtual:zod-compiler/runtime\" referenced in this schema (lean mode only). */\n usedHelpers: Set<string>;\n /**\n * Name of the fast-path boolean helper for the ROOT recursion target\n * (refId 0), allocated on first fastRecursiveRef visit. generateValidator\n * wraps the root fast expression as `function <name>(input){return <expr>;}`\n * so recursive refs can call it. undefined = root has no recursion on the\n * fast path.\n */\n recFastName?: string;\n /**\n * Hosted-validator name table for recursion targets, keyed by refId. Entry 0\n * is the root (the schema's own `safeParse_<name>` / `recFastName`); entries\n * ≥ 1 are non-root targets hosted as standalone `__rsp_N` (slow) / `__fcr_N`\n * (fast) helpers. `recursiveRef`/`recursionTarget` generators look up the\n * call target here. Undefined when not generating a full validator (e.g. unit\n * tests calling a single generator) — treated as root-only.\n */\n recTargets?: Map<number, RecTargetGen>;\n /** Dedup cache for hosted zero-capture effect functions: source text → preamble var. */\n effectFnCache?: Map<string, string>;\n /** Dedup cache for constant preamble declarations: initializer text → preamble var. */\n valueCache?: Map<string, string>;\n /** Name of the build path's FAIL sentinel, declared once per validator. */\n buildFailName?: string;\n /** Hosted build-function name per recursion target refId, so back-edges resolve. */\n buildRecNames?: Map<number, string>;\n /**\n * Set by the build path when it emitted a `.default()` substitution. Such a\n * schema ACCEPTS an input its fast expression rejects — `fastDefault` demands a\n * present value, since the fast path's contract is `data === input` and a\n * substituted default is not the input — so the expression is no longer an\n * exact acceptance predicate and must not be installed as `.is()`. Stripping,\n * by contrast, reshapes only the payload, which is why a build-path schema\n * otherwise still hands its predicate over.\n */\n buildSubstitutesValue?: boolean;\n /** Memo for estimateFastCost (size-gated fast-check extraction). Lazily created. */\n fastSizeCache?: WeakMap<SchemaIR, number>;\n /** Memo for estimateRuntimeCost (cheapest-first check ordering). Lazily created. */\n fastRuntimeCostCache?: WeakMap<SchemaIR, number>;\n /**\n * File-level shared slow-walk plan. Set only when generating a mutation-free\n * schema (so shared walks stay on the deferred cold path); the slow-path\n * visit() consults it to replace a repeated sub-IR with a `__zcSw_N` call.\n */\n sharedSchemas?: SharedSchemaPlan;\n}\n\n// ─── Slow Path context ────────────────────────────────────────────────────────\n\n/** Context object for slow-path (error-collecting) generator functions. */\nexport interface SlowGen {\n readonly input: string;\n readonly output: string;\n readonly path: string;\n readonly issues: string;\n readonly ctx: CodeGenContext;\n /**\n * Schema-level static error message of the node being generated\n * (z.string({ error: \"...\" })). Default message for issues this node emits\n * when the individual check has no message of its own. Set by generateSlow()\n * from ir.typeMessage; never inherited by child nodes.\n */\n readonly typeMsg?: string | undefined;\n\n /**\n * Name of a boolean variable that this node sets to `true` when it aborts in\n * the zod sense (`payload.aborted`) — currently only a pipe/codec whose `in`\n * step fails (zod's `handlePipeResult` sets `left.aborted = true`). A `union`\n * allocates one per option and reads it during pruning so a pipe option whose\n * `in` failed counts as aborted even when its only issue is a non-aborting\n * `custom`/check-level code. Undefined when the node is not inside an\n * abort-tracking option, in which case the abort is a no-op.\n *\n * Unlike input/output/path/issues, this is NOT inherited by `visit()`: it is\n * cleared at every boundary unless a node explicitly forwards it (the\n * pass-through wrappers optional/nullable/readonly do), mirroring how zod\n * propagates `payload.aborted` through transparent wrappers but not across\n * container boundaries.\n */\n readonly aborted?: string | undefined;\n\n /**\n * Recursively generate validation for a child IR node.\n * input/output/path/issues are inherited from parent unless overridden;\n * `aborted` is the exception — it is only set when explicitly passed (see the\n * `aborted` field doc), so it never leaks into container children.\n * Union generators use `{ issues }` to redirect child errors to temporary arrays.\n * Container generators use `{ input, output, path }` for element traversal.\n */\n visit(\n ir: SchemaIR,\n overrides?: {\n input?: string;\n output?: string;\n path?: string;\n issues?: string;\n // `| undefined` (unlike the others): pass-through wrappers forward\n // `g.aborted` verbatim, which is undefined outside an abort-tracking option.\n aborted?: string | undefined;\n },\n ): string;\n\n /** Generate a unique temp variable name: `__${prefix}_${counter++}` */\n temp(prefix: string): string;\n\n /** Add a regex to preamble and return the variable name. */\n regex(prefix: string, pattern: string, flags?: string): string;\n\n /** Add a Set to preamble and return the variable name. */\n set(prefix: string, values: readonly unknown[]): string;\n}\n\n/** Slow-path generator function signature — registered in slowRegistry. */\nexport type SlowGenerator<T extends SchemaIR = SchemaIR> = (ir: T, g: SlowGen) => string;\n\n// ─── Fast Path context ────────────────────────────────────────────────────────\n\n/**\n * Per-emitted-function size accumulator for size-gated fast-check extraction.\n * Shared by every node inlined into the same function; a fresh instance starts\n * each hosted helper (and the root). See fast-size.ts / generateFast.\n */\nexport interface FastScope {\n used: number;\n /**\n * `var` temps that the function this scope is assembling must declare, in\n * allocation order. Populated by {@link FastGen.local}; every site that\n * materializes a function body from a fresh scope emits\n * {@link declareFastTemps} at the top of that body.\n *\n * Function-scoped (never module-scoped) is load-bearing: a recursive\n * validator re-enters itself while an outer frame still holds a live temp,\n * and each invocation needs its own binding.\n */\n temps: string[];\n}\n\n/** `var a,b;` declaration for a scope's temps, or \"\" when it allocated none. */\nexport function declareFastTemps(scope: FastScope): string {\n return scope.temps.length > 0 ? `var ${scope.temps.join(\",\")};` : \"\";\n}\n\n/**\n * Fast check for a wrapper that compares its input against a sentinel and\n * otherwise delegates to an inner schema — `optional` (`===undefined`),\n * `nullable` (`===null`) and `default` (`!==undefined`).\n *\n * Written naively these read the input TWICE: once for the sentinel test and\n * again inside the inner check (which may itself read it several more times —\n * `typeof x===\"string\"&&x.length>=3&&x.length<=20`). V8's load elimination\n * removes the repeats only while the access is monomorphic; on the polymorphic\n * and megamorphic call sites real payloads produce (an array of objects with\n * differing key order, anything out of `JSON.parse`) every repeat is a fresh\n * megamorphic lookup. Binding the value to a local once is worth 1.1-1.7x on\n * the whole object check when the optional key is present, and is neutral when\n * it is absent.\n *\n * Only hoisted when the input is a property access; a bare local (an array\n * element variable, a record value) is already a single load, so it keeps the\n * shorter form and byte-identical output.\n */\nexport function fastSentinelWrapper(\n g: FastGen,\n innerIR: SchemaIR,\n sentinel: string,\n joiner: \"&&\" | \"||\",\n): string | null {\n if (!isPropertyAccess(g.input)) {\n const inner = g.visit(innerIR);\n return inner === null ? null : `(${g.input}${sentinel}${joiner}(${inner}))`;\n }\n const value = g.local(\"w\");\n const inner = g.visit(innerIR, { input: value });\n if (inner === null) return null;\n return `((${value}=${g.input})${sentinel}${joiner}(${inner}))`;\n}\n\n/** True for an expression that performs a property load (`x[\"a\"]`, `x.a`, `x[0][1]`). */\nfunction isPropertyAccess(expr: string): boolean {\n return expr.includes(\"[\") || expr.includes(\".\");\n}\n\n/** Context object for fast-path (boolean expression) generator functions. */\nexport interface FastGen {\n readonly input: string;\n readonly ctx: CodeGenContext;\n\n /**\n * Whether the CURRENT node may be hoisted into its own boolean helper when it\n * (with the already-emitted siblings) would overflow the function size cap.\n * False for the root and for a helper's own top node — those are already their\n * own function — but their children are extractable. See generateFast.\n */\n readonly extractable: boolean;\n\n /** Accumulated size (≈ chars) of the function currently being assembled. */\n readonly scope: FastScope;\n\n /**\n * Set on the gen for a discriminated-union option only: the discriminator\n * key. Signals `fastObject` to omit its type-guard and skip re-checking that\n * property (the switch already matched its value). Never propagated to child\n * nodes — nested objects keep their own guard.\n */\n readonly discSkipKey?: string | undefined;\n\n /**\n * Recursively generate fast-check expression for a child IR node.\n * Returns null if any child is ineligible for fast path.\n */\n visit(ir: SchemaIR, overrides?: { input?: string; discSkipKey?: string }): string | null;\n\n /**\n * A FastGen for emitting a SEPARATE function body (a hand-built preamble\n * helper such as a discriminated-union switch or an array-element loop). It\n * carries a FRESH size accumulator, so the helper's own content is size-gated\n * against the cap independently of the caller — without this, a helper's body\n * accrues to the caller's scope while the helper itself grows unbounded.\n */\n scoped(input: string): FastGen;\n\n /** Generate a unique temp variable name. */\n temp(prefix: string): string;\n\n /**\n * Allocate a unique name AND record it on this scope so the enclosing\n * emitted function declares it as a `var` (see {@link FastScope.temps}).\n * Use for a value bound inside an expression — `(t=x[\"k\"])===undefined` —\n * where `temp()` alone would leave the name undeclared.\n */\n local(prefix: string): string;\n\n /** Add a regex to preamble and return the variable name. */\n regex(prefix: string, pattern: string, flags?: string): string;\n}\n\n/** Fast-path generator function signature — registered in fastRegistry. */\nexport type FastGenerator<T extends SchemaIR = SchemaIR> = (ir: T, g: FastGen) => string | null;\n\n// ─── Shared emit helpers (used by both slow-path and fast-path factories) ────\n\n/** Allocate a fresh `__${prefix}_${n}` identifier and bump the shared counter. */\nexport function emitTemp(ctx: CodeGenContext, prefix: string): string {\n return `__${prefix}_${ctx.counter++}`;\n}\n\n/**\n * Host a zero-capture effect function (refine predicate, transform,\n * overwrite) in the preamble and return its variable name. The inline\n * `(${source})(x)` form evaluates the function expression — allocating a\n * function object — on EVERY parse at every effect site, including inside\n * the \"zero-allocation\" fast chain. V8's escape analysis erases that in\n * optimized frames, but interpreter/baseline/deopt frames pay it, and the\n * full source text re-parses as bytecode at each site. Zero-capture sources\n * reference only their own parameters and safe globals by construction, so\n * a single preamble binding is semantically identical. Deduped per schema\n * by source text.\n */\nexport function emitEffectFn(ctx: CodeGenContext, source: string): string {\n ctx.effectFnCache ??= new Map();\n const cached = ctx.effectFnCache.get(source);\n if (cached !== undefined) return cached;\n const name = `__ef_${ctx.counter++}`;\n ctx.preamble.push(`var ${name}=(${source});`);\n ctx.effectFnCache.set(source, name);\n return name;\n}\n\n/**\n * Callable expression for a user callback — a refine predicate or a transform.\n *\n * A zero-capture callback is hosted from its source text; one that CAPTURES\n * outer variables is called by reference through `__rf[N]` — the user's own\n * function object, reached from the schema — instead of costing the schema its\n * compiled path. The reference is aliased into a preamble binding rather than\n * re-read per call, for the same reason call-invoked helpers are (a per-call\n * array element load is not a foldable callee).\n */\nexport function emitEffectCallable(\n ctx: CodeGenContext,\n effect: { refIndex?: number | undefined; source?: string | undefined },\n): string {\n if (effect.refIndex !== undefined) return emitConstant(ctx, \"rfn\", `__rf[${effect.refIndex}]`);\n if (effect.source === undefined) {\n throw new Error(\"effect has neither inlineable source nor a reference index\");\n }\n return emitEffectFn(ctx, effect.source);\n}\n\n/**\n * Pristine fallback delegate: declare `var __rfp_N=__rf[N].safeParse.bind(__rf[N]);`\n * in the preamble and return the variable name. Generated code must NEVER read\n * `__rf[N].safeParse` at parse time: `__zcMkv` installs the compiled safeParse as\n * an OWN property on the original schema object, and whenever `__rf[N]` is that\n * same object the read resolves to the compiled delegate itself — infinite\n * recursion (RangeError on every parse). The fallback entry and the __zcMkv\n * target ARE the same object in compile mode (schemaExpr is the compile()\n * argument identifier) and the CLI emitter ((__src_X as any).schema); in\n * autoDiscover mode they are two textually identical constructions that any\n * downstream CSE/dedup transform (babel-plugin-zod-hoist in a field incident)\n * collapses back into one. Capturing at IIFE evaluation — before the trailing\n * `return __zcMkv(...)` mutates anything — pins zod's own implementation; the\n * worst case under cross-validator merges is delegating to an equivalent\n * compiled validator (whose own delegates were captured even earlier), never\n * a cycle.\n */\nexport function emitRfDelegate(ctx: CodeGenContext, refIndex: number): string {\n const name = `__rfp_${refIndex}`;\n const decl = `var ${name}=__rf[${refIndex}].safeParse.bind(__rf[${refIndex}]);`;\n if (!ctx.preamble.includes(decl)) {\n ctx.preamble.push(decl);\n }\n return name;\n}\n\n/**\n * Resolve a regex pattern to a runtime variable name.\n * Lean mode short-circuits well-known patterns to virtual-module names so the\n * bundler can dedup across files; everything else is cached + declared in the\n * per-IIFE preamble exactly once per pattern.\n */\nexport function emitRegex(\n ctx: CodeGenContext,\n prefix: string,\n pattern: string,\n flags?: string,\n): string {\n if (ctx.mode === \"lean\" && !flags) {\n const wellKnown = lookupWellKnownRegex(pattern);\n if (wellKnown !== null) {\n ctx.usedHelpers.add(wellKnown);\n return wellKnown;\n }\n }\n const cacheKey = flags ? `${flags}\\u0000${pattern}` : pattern;\n const cached = ctx.regexCache.get(cacheKey);\n if (cached) return cached;\n const name = `__re_${prefix}_${ctx.counter++}`;\n const flagsArg = flags ? `,${escapeString(flags)}` : \"\";\n // Flag-less patterns may carry a faster behavior-equivalent rewrite (a\n // well-known table entry, repeat unrolling, or both); the regex OBJECT uses\n // it while issue sites keep reporting the original pattern (see slowString).\n const testSource = flags ? null : fastTestSource(pattern);\n ctx.preamble.push(`var ${name}=new RegExp(${escapeString(testSource ?? pattern)}${flagsArg});`);\n ctx.regexCache.set(cacheKey, name);\n return name;\n}\n\n/**\n * Resolve the ORIGINAL `/source/flags` pattern string of a regex for issue\n * reporting. Only needed when emitRegex swapped in a faster equivalent test\n * pattern (the runtime regex's toString() would leak the rewrite). Lean mode\n * references the shared `<name>Src` virtual export so the original pattern\n * stays a single bundle-wide string; inline mode declares it once per IIFE.\n */\nexport function emitRegexSourceString(ctx: CodeGenContext, pattern: string): string {\n if (ctx.mode === \"lean\") {\n const srcName = wellKnownRegexSourceName(pattern);\n if (srcName !== null) {\n ctx.usedHelpers.add(srcName);\n return srcName;\n }\n }\n const cacheKey = `src\\u0000${pattern}`;\n const cached = ctx.regexCache.get(cacheKey);\n if (cached) return cached;\n const name = `__res_${ctx.counter++}`;\n ctx.preamble.push(`var ${name}=${escapeString(`/${pattern}/`)};`);\n ctx.regexCache.set(cacheKey, name);\n return name;\n}\n\n/**\n * Declare a constant value in the preamble and return its variable name,\n * reusing an earlier declaration of the SAME initializer.\n *\n * Value tables are reached from both halves of a validator — an enum's `Set`\n * from its fast check and again from its slow walk, a strict shape's key table\n * likewise — and repeat across sibling properties that share a value list. One\n * declaration per USE emitted the payload two or four times: measured 17% of a\n * 20-value enum schema's generated bytes, 16% for an object with two identical\n * enums. Keyed by initializer text, so only identical payloads collapse.\n */\nexport function emitConstant(ctx: CodeGenContext, prefix: string, initializer: string): string {\n ctx.valueCache ??= new Map();\n const cached = ctx.valueCache.get(initializer);\n if (cached !== undefined) return cached;\n const name = `__${prefix}_${ctx.counter++}`;\n ctx.preamble.push(`var ${name}=${initializer};`);\n ctx.valueCache.set(initializer, name);\n return name;\n}\n\n/** Declare a `new Set([...])` in the preamble and return its variable name. */\nexport function emitSet(ctx: CodeGenContext, prefix: string, values: readonly unknown[]): string {\n return emitConstant(ctx, `set_${prefix}`, `new Set(${JSON.stringify([...values])})`);\n}\n\n/**\n * Shape-key count at or below which the unknown-key pass compares with an\n * inline `===` chain rather than a hashed lookup.\n *\n * This is deliberately NOT {@link ENUM_INLINE_THRESHOLD}: the two look alike but\n * are different workloads. An enum compares schema literals against arbitrary\n * INPUT strings, which may be long, share prefixes, and are not necessarily\n * internalized — so a hashed set earns its keep quickly. A shape-key test\n * compares them against keys arriving from `for-in`, i.e. the object's own\n * internalized key strings, so every arm of the chain is a pointer compare that\n * V8 predicts perfectly, while `table[k]` / `set.has(k)` pays a string hash and\n * probe per key.\n *\n * Measured over a strict object's for-in pass (JSON-parsed input, 8 rotated\n * shapes), `===` chain vs the previous `{k:1}` table: 3.7x at 6 keys, 3.8x at\n * 10, 3.1x at 20, 3.1x at 48 — the chain still leads at 64 (283 ns vs 690) and\n * only loses past ~96, where `Set.has` (not the table, which never wins at any\n * size) takes over. 64 sits below that crossover and above any realistic shape.\n */\nexport const KEY_MEMBERSHIP_INLINE_THRESHOLD = 64;\n\n/**\n * Boolean membership test for one key variable against a fixed key list.\n * Empty list recognizes nothing.\n *\n * `Set.has` is the large-shape fallback rather than a `{key:1}` object table:\n * the table is also `__proto__`-hostile (an own `__proto__` key cannot be set\n * by an object literal, so that key would silently read as unknown), which the\n * Set has no trouble with.\n */\nexport function keyMembershipTest(\n ctx: CodeGenContext,\n keys: readonly string[],\n keyVar: string,\n): string {\n if (keys.length === 0) return \"false\";\n if (keys.length <= KEY_MEMBERSHIP_INLINE_THRESHOLD) {\n return keys.map((k) => `${keyVar}===${escapeString(k)}`).join(\"||\");\n }\n return `${emitSet(ctx, \"ks\", keys)}.has(${keyVar})`;\n}\n\n/**\n * Enum values at or below this count use inline === checks instead of Set.has().\n * Measured on V8: for ≤5 values, an === chain beats Set.has by up to ~3x with\n * realistic (distinct-prefix, JSON-parsed) values — V8 internalizes strings on\n * successful comparison, making subsequent arms pointer-equality — and is no\n * worse than Set.has even with adversarial shared-prefix values.\n */\nexport const ENUM_INLINE_THRESHOLD = 5;\n\nconst CHECK_PRIORITY: Record<string, number> = {\n // Cheapest: length/size comparisons (O(1))\n min_length: 10,\n max_length: 11,\n length_equals: 12,\n min_size: 13,\n max_size: 14,\n // Number format checks (comparison + bitwise)\n number_format: 15,\n // Range comparisons\n greater_than: 20,\n less_than: 21,\n bigint_greater_than: 20,\n bigint_less_than: 21,\n date_greater_than: 22,\n date_less_than: 23,\n // Modulo\n multiple_of: 30,\n bigint_multiple_of: 30,\n // String prefix/suffix (O(prefix/suffix length))\n starts_with: 40,\n ends_with: 41,\n // String search (O(n·m) worst case)\n includes: 42,\n // Regex (most expensive)\n string_format: 50,\n};\n\nexport function escapeString(s: string | number): string {\n return JSON.stringify(s);\n}\n\n/**\n * JS source for a primitive literal value (literal schemas, discriminator\n * case labels). JSON.stringify covers string/number/boolean/null; bigint\n * needs the `n` suffix (JSON.stringify throws and String(5n) renders a\n * number literal that never strict-equals a bigint); undefined isn't JSON.\n */\nexport function literalToJs(v: string | number | boolean | null | bigint | undefined): string {\n if (typeof v === \"bigint\") return `${v}n`;\n if (v === undefined) return \"undefined\";\n // JSON.stringify maps NaN/±Infinity to \"null\"; emit them as JS expressions so a\n // non-finite numeric literal round-trips (z.literal(Infinity) must compare\n // against Infinity, not null). String(NaN)=\"NaN\", String(Infinity)=\"Infinity\",\n // String(-Infinity)=\"-Infinity\" — all valid JS that evaluate to the value.\n if (typeof v === \"number\" && !Number.isFinite(v)) return String(v);\n return JSON.stringify(v);\n}\n\n/**\n * Helpers that generated code invokes through `Function.prototype.call`, and\n * which therefore must be aliased into a module-local binding in lean mode.\n *\n * V8 folds a local `const` callee into a constant and inlines straight through\n * `x.call(...)`; an IMPORTED binding is a cell it will not fold, so the same\n * expression stays a generic property load plus a generic call — measured 4.5x\n * (5 keys) to 6.5x (20 keys) slower on the record fast path, 35.9 ns vs 7.2 ns\n * for a 5-key record. Aliasing the import into the IIFE recovers all of it\n * (7.4 ns). A DIRECT call to an imported function (`__zcFsr(v,s)`) is not\n * penalized — measured identical — and neither is an imported RegExp receiver,\n * so only the `.call` sites are listed here.\n */\nconst CALL_INVOKED_HELPERS: ReadonlySet<string> = new Set([\"__zcHop\"]);\n\n/**\n * Reference a shared runtime helper (e.g. __zcFsr) from generated code.\n * Lean mode: registers it for the `virtual:zod-compiler/runtime` import.\n * Inline mode: declares it once in the per-IIFE preamble.\n */\nexport function emitRuntimeHelper(ctx: CodeGenContext, name: string, decl: string): string {\n if (ctx.mode === \"lean\") {\n ctx.usedHelpers.add(name);\n if (CALL_INVOKED_HELPERS.has(name)) return emitConstant(ctx, \"lh\", name);\n } else if (!ctx.preamble.includes(decl)) {\n ctx.preamble.push(decl);\n }\n return name;\n}\n\n/**\n * Extend a path expression with one or more scalar segment expressions\n * (escaped string literals, numeric literals, or loop-variable names).\n *\n * Path expressions are only ever composed by these helpers starting from the\n * `[]` root, so any path that looks like an array literal IS one — the new\n * segment is spliced in to keep issue paths a single array allocation\n * (`[\"data\",\"items\",__i_7]`) instead of an allocation per nesting level\n * (`[\"data\"].concat(\"items\").concat(__i_7)`). Opaque expressions fall back\n * to .concat().\n */\nexport function extendPath(parentPath: string, segExpr: string): string {\n if (parentPath === \"[]\") return `[${segExpr}]`;\n if (parentPath.startsWith(\"[\") && parentPath.endsWith(\"]\")) {\n return `${parentPath.slice(0, -1)},${segExpr}]`;\n }\n return `${parentPath}.concat(${segExpr})`;\n}\n\n/** Extend a path expression with a static string key. */\nexport function extendStaticPath(parentPath: string, key: string): string {\n return extendPath(parentPath, escapeString(key));\n}\n\n/** Extend a path expression with a numeric index. */\nexport function extendStaticPathIndex(parentPath: string, index: number): string {\n return extendPath(parentPath, String(index));\n}\n\n/**\n * A superRefine callback receives zod's payload, whose `value` is public,\n * typed, writable API ($RefinementCtx extends ParsePayload) — so any node\n * carrying one MAY rewrite its value and must be treated as mutating. Which\n * callbacks actually do is undecidable here; the emitted fast check settles it\n * at runtime by refusing when the value changed (see ZC_SR_OK_DECL), so a\n * non-mutating callback still exits through the fast path.\n */\nfunction hasSuperRefine(checks: readonly { kind: string }[] | undefined): boolean {\n return checks !== undefined && checks.some((c) => c.kind === \"super_refine_effect\");\n}\n\n/**\n * Check if a SchemaIR tree produces output that is not the input itself —\n * either value-mutating operations (coerce, default, catch, overwrite) that\n * write back to the input expression, or a strip object that rebuilds a fresh\n * object from its known keys. Used by container generators to decide whether to\n * clone (so the rebuilt/mutated value never writes through to the caller's\n * input), by generateValidator to keep such schemas off the by-reference fast\n * path, and by the shared-walk dedup + intersection extractor to exclude them.\n */\nexport function hasMutation(ir: SchemaIR): boolean {\n switch (ir.type) {\n case \"string\":\n // url checks trim (and optionally normalize) the value; overwrite\n // effects (.trim(), .toLowerCase()) rewrite it.\n return (\n ir.coerce === true ||\n hasSuperRefine(ir.checks) ||\n ir.checks.some(\n (c) =>\n c.kind === \"overwrite_effect\" || (c.kind === \"string_format\" && c.format === \"url\"),\n )\n );\n case \"number\":\n return ir.coerce === true || hasSuperRefine(ir.checks);\n case \"boolean\":\n case \"bigint\":\n case \"date\":\n return ir.coerce === true;\n case \"default\":\n case \"catch\":\n case \"effect\":\n case \"fallback\":\n case \"stringBool\":\n return true;\n case \"object\":\n // A strip object produces a FRESH output (only the declared keys), so it\n // mutates: parents must clone before it writes back, it never takes the\n // by-reference fast path, and intersections of strip objects delegate to\n // zod (see extractIntersection's hasMutation guard) — matching zod's\n // parse-both-sides-then-merge semantics instead of over-stripping.\n return (\n ir.stripUnknownKeys === true ||\n hasSuperRefine(ir.checks) ||\n (ir.catchall !== undefined && hasMutation(ir.catchall)) ||\n Object.values(ir.properties).some((p) => hasMutation(p))\n );\n case \"array\":\n return hasSuperRefine(ir.checks) || hasMutation(ir.element);\n case \"tuple\":\n return ir.items.some(hasMutation) || (ir.rest !== null && hasMutation(ir.rest));\n case \"record\":\n return hasMutation(ir.valueType);\n case \"optional\":\n case \"nullable\":\n case \"readonly\":\n case \"recursionTarget\":\n case \"zodDelegate\":\n return hasMutation(ir.inner);\n case \"union\":\n case \"discriminatedUnion\":\n return ir.options.some(hasMutation);\n case \"intersection\":\n return hasMutation(ir.left) || hasMutation(ir.right);\n case \"pipe\":\n return hasMutation(ir.in) || hasMutation(ir.out);\n case \"set\":\n return hasMutation(ir.valueType);\n case \"map\":\n return hasMutation(ir.keyType) || hasMutation(ir.valueType);\n case \"file\":\n return false;\n default:\n return false;\n }\n}\n\n/**\n * Is a defaulted property's key guaranteed to appear in the stripped output?\n *\n * Distinct from {@link rejectsUndefined}, which asks whether `undefined` is\n * REJECTED — a `.default()` accepts it and yet still produces a defined value, so\n * only this question earns the key a slot in the output object literal. The two\n * answers coincide everywhere else.\n *\n * Sound for both branches of a default: the substituted value is defined\n * (`alwaysDefined`, checked against the schema at extraction time), and the inner\n * branch runs only when `input[key] !== undefined`, which implies `key in input`\n * — so zod's presence test keeps the key whatever the inner produced.\n */\nexport function outputAlwaysDefined(ir: SchemaIR): boolean {\n return ir.type === \"default\" ? ir.alwaysDefined === true : rejectsUndefined(ir);\n}\n\n/**\n * Does this schema reject `undefined` outright?\n *\n * Read as \"can this slot be ABSENT from the input\" by the tuple build, whose\n * output length depends on it — zod marks a defaulted or optional item\n * `optin: \"optional\"` and accepts a shorter array. Conservative: anything that\n * might accept, produce, or default to `undefined` answers false.\n */\nexport function rejectsUndefined(ir: SchemaIR): boolean {\n switch (ir.type) {\n // Coercion turns undefined into a value (`String(undefined)`), so a\n // coercing primitive is NOT a rejector.\n case \"string\":\n case \"number\":\n case \"boolean\":\n case \"bigint\":\n case \"date\":\n return ir.coerce !== true;\n case \"symbol\":\n case \"null\":\n case \"nan\":\n case \"never\":\n case \"enum\":\n case \"object\":\n case \"array\":\n case \"tuple\":\n case \"record\":\n case \"set\":\n case \"map\":\n case \"file\":\n case \"templateLiteral\":\n case \"discriminatedUnion\":\n case \"stringBool\":\n return true;\n case \"literal\":\n return !ir.values.includes(undefined);\n case \"union\":\n return ir.options.every(rejectsUndefined);\n case \"intersection\":\n return rejectsUndefined(ir.left) || rejectsUndefined(ir.right);\n case \"nullable\":\n case \"readonly\":\n case \"recursionTarget\":\n case \"zodDelegate\":\n return rejectsUndefined(ir.inner);\n default:\n // optional / any / unknown / undefined / void / default / catch /\n // fallback / effect / pipe / recursiveRef — each can yield undefined,\n // or is opaque enough that we must not assume otherwise.\n return false;\n }\n}\n\n/**\n * Sort comparator for CheckIR: cheapest/most-discriminating checks first.\n * Used by fast-path generators after filtering out refine_effect entries.\n */\nexport function checkPriority(\n a: CheckIR | BigIntCheckIR | DateCheckIR | SetCheckIR,\n b: CheckIR | BigIntCheckIR | DateCheckIR | SetCheckIR,\n): number {\n return (CHECK_PRIORITY[a.kind] ?? 99) - (CHECK_PRIORITY[b.kind] ?? 99);\n}\n"],"mappings":"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"}
|
|
@@ -78,6 +78,7 @@ function referencesExportRefs(ir) {
|
|
|
78
78
|
case "catch":
|
|
79
79
|
case "fallback": return true;
|
|
80
80
|
case "custom": return true;
|
|
81
|
+
case "zodDelegate": return true;
|
|
81
82
|
case "effect":
|
|
82
83
|
if (ir.refIndex !== void 0) return true;
|
|
83
84
|
break;
|
|
@@ -121,7 +122,8 @@ function childSchemas(ir) {
|
|
|
121
122
|
case "catch": return [ir.inner];
|
|
122
123
|
case "pipe": return [ir.in, ir.out];
|
|
123
124
|
case "effect":
|
|
124
|
-
case "recursionTarget":
|
|
125
|
+
case "recursionTarget":
|
|
126
|
+
case "zodDelegate": return [ir.inner];
|
|
125
127
|
default: return [];
|
|
126
128
|
}
|
|
127
129
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.js","names":[],"sources":["../../../src/core/codegen/dedupe.ts"],"sourcesContent":["/**\n * File-level schema deduplication.\n *\n * When several schemas in one file share structurally identical sub-trees\n * (a reused `Address` object, a `{ id, name }` row, an exported schema that is\n * also embedded in another), the error-collecting \"slow walk\" for that shape\n * is generated ONCE as a shared `__zcSw_N(input, path, issues)` function and\n * every occurrence calls it instead of inlining a fresh copy.\n *\n * Two invariants keep this free of runtime cost and footguns:\n *\n * 1. **Slow walk only.** The fast path (the `&&` boolean chain that validates\n * valid input with zero allocations) is NEVER shared — it stays fully\n * inlined so the hot path is byte-for-byte what it was before. Only the\n * cold, bulky error walk (64–77% of generated bytes) is shared.\n *\n * 2. **No per-export references.** A shared body lives at MODULE scope, while\n * `__rf[]` — the array of retained Zod schemas and captured callbacks — is\n * declared inside each export's IIFE. So a shape may be shared only when\n * nothing in it resolves through `__rf`: no `default`, `catch`, `fallback`,\n * captured `transform`, or captured `refine`/`superRefine`. Zero-capture\n * callbacks are fine — they are hosted from source text into the shared\n * block's own preamble.\n *\n * Sharing used to additionally require the whole ROOT to be mutation-free, so\n * that a body which wrote nothing back could not lose an output value. That\n * excluded a genuine `z.object()` — it strips, so its walk PRODUCES a rebuilt\n * object — which, once `z.object()` began always stripping, meant the feature\n * stopped firing for essentially every real schema: a realistic module of four\n * schemas reusing two sub-shapes shared nothing and shipped 29.6 KB where the\n * `strictObject` spelling of the same thing shipped 16.7 KB.\n *\n * The write-back problem is solved directly instead: a shared walk RETURNS its\n * value and call sites assign it (`slot = __zcSw_N(slot, path, issues)`), so a\n * stripping object, a coercion or a `.trim()` delivers its rewritten value\n * through the call exactly as an inlined walk delivers it through `g.output`.\n */\n\nimport type { SchemaIR } from \"../types.js\";\nimport type { CodeGenContext, CodegenMode } from \"./context.js\";\nimport { createSlowGen, generateSlow } from \"./slow-path.js\";\n\n/** A shared slow-walk validator hosted at module scope. */\nexport interface SharedSchemaRef {\n /** Name of the `function __zcSw_N(input, path, issues)` declaration. */\n readonly name: string;\n}\n\nexport interface SharedSchemaPlan {\n /**\n * Shared ref for an IR node, or undefined when the node is not a shared\n * candidate. Keyed by structural identity (memoized), so repeated lookups\n * during codegen are O(1).\n */\n refFor(ir: SchemaIR): SharedSchemaRef | undefined;\n /** Module-scope declarations (shared preamble + `__zcSw_N` functions). `\"\"` when empty. */\n readonly code: string;\n /** Runtime helper names referenced by the shared block (lean mode). */\n readonly usedHelpers: Set<string>;\n readonly isEmpty: boolean;\n}\n\n/**\n * Minimum subtree weight (≈ node count, +1 per check) for a repeated shape to\n * be worth a shared function. Below this the call + dynamic-path overhead\n * outweighs the duplicated inline walk; ~4 admits a 3-field object or an\n * array-of-object while skipping bare primitives and thin wrappers.\n */\nconst MIN_SHARED_WEIGHT = 4;\n\n/** Leading marker of a shared block; used to skip re-injection on watch/HMR re-runs. */\nexport const SHARED_BLOCK_MARKER = \"/* zod-compiler shared */\";\n\nconst EMPTY_PLAN: SharedSchemaPlan = {\n refFor: () => undefined,\n code: \"\",\n usedHelpers: new Set(),\n isEmpty: true,\n};\n\n/**\n * Build the shared-schema plan for one file's root schemas. Returns\n * {@link EMPTY_PLAN} (a no-op) when nothing repeats — the common case, so files\n * without internal duplication pay only a single linear analysis pass.\n */\nexport function createSharedSchemaPlan(\n roots: readonly SchemaIR[],\n mode: CodegenMode,\n): SharedSchemaPlan {\n const keyMemo = new Map<SchemaIR, string>();\n const weightMemo = new Map<SchemaIR, number>();\n const stats = new Map<string, { ir: SchemaIR; count: number; weight: number }>();\n\n for (const root of roots) {\n collect(root, keyMemo, weightMemo, stats);\n }\n\n const selected = [...stats.values()]\n .filter(\n (c) =>\n c.count >= 2 &&\n c.weight >= MIN_SHARED_WEIGHT &&\n !containsRecursiveRef(c.ir) &&\n !referencesExportRefs(c.ir),\n )\n // Largest shapes first so the biggest wins get the lowest indices; tie-break\n // on the structural key for deterministic, reproducible output.\n .sort((a, b) => b.weight - a.weight || cmp(keyOf(a.ir, keyMemo), keyOf(b.ir, keyMemo)));\n\n if (selected.length === 0) return EMPTY_PLAN;\n\n const refs = new Map<string, SharedSchemaRef>();\n selected.forEach((c, i) => refs.set(keyOf(c.ir, keyMemo), { name: `__zcSw_${i}` }));\n\n const refFor = (ir: SchemaIR): SharedSchemaRef | undefined => refs.get(keyOf(ir, keyMemo));\n\n const ctx: CodeGenContext = {\n preamble: [],\n counter: 0,\n fnName: \"__zcShared\",\n regexCache: new Map(),\n mode,\n usedHelpers: new Set(),\n };\n // Generation consults the plan so a shared candidate nested inside another\n // shared candidate becomes a `__zcSw_*` call rather than a second inline copy.\n // Containment is strictly size-decreasing, so the shared functions form a DAG;\n // hoisted declarations make call order irrelevant.\n ctx.sharedSchemas = { refFor, code: \"\", usedHelpers: ctx.usedHelpers, isEmpty: false };\n\n const fns: string[] = [];\n for (const c of selected) {\n const ref = refs.get(keyOf(c.ir, keyMemo));\n if (ref === undefined) continue;\n // Generate the candidate's body INLINE (the root is passed to generateSlow,\n // not visit(), so it never replaces itself with a call); only its children\n // route through visit() and may resolve to other shared refs.\n const body = generateSlow(c.ir, createSlowGen(\"input\", \"input\", \"path\", \"_e\", ctx));\n // `input` is both the read and the write slot (createSlowGen above passes it\n // as input AND output), so a walk that rewrites — a stripping object, a\n // coercion, an overwrite check — leaves its result there. Returning it is\n // what lets the caller receive that value; a walk that rewrites nothing\n // returns its argument unchanged.\n fns.push(`function ${ref.name}(input,path,_e){\\n${body}\\nreturn input;\\n}`);\n }\n\n return {\n refFor,\n code: [SHARED_BLOCK_MARKER, ...ctx.preamble, ...fns].join(\"\\n\"),\n usedHelpers: ctx.usedHelpers,\n isEmpty: false,\n };\n}\n\n// A shared sub-schema is called as `slot = __zcSw_N(slot, path, issues)`: the\n// walk pushes directly into the caller's `issues` array (no result object, no\n// per-call issue copy) and returns the parsed value, which the call site assigns\n// back into the slot the inlined walk would have written. Call sites are emitted\n// by slow-path.ts (nested occurrences) and index.ts (a shared root), keyed off\n// `ref.name`.\n\n/**\n * Does the subtree resolve anything through the per-export `__rf[]` array —\n * a retained Zod schema (`default`, `catch`, `fallback`) or a captured callback\n * (`transform`, `refine`, `superRefine`)?\n *\n * Those names exist only inside an export's own IIFE, so a module-scope shared\n * body referencing them would be unbound. A zero-capture callback carries\n * `source` instead of `refIndex` and is hosted into the shared block's preamble,\n * so it does not disqualify a shape.\n */\nfunction referencesExportRefs(ir: SchemaIR): boolean {\n switch (ir.type) {\n case \"default\":\n case \"catch\":\n case \"fallback\":\n // `refIndex` is mandatory on these — the value/schema lives on the export.\n return true;\n case \"custom\":\n // Both the predicate and pristine error delegate live on the export.\n return true;\n case \"effect\":\n if (ir.refIndex !== undefined) return true;\n break;\n case \"string\":\n case \"number\":\n case \"object\":\n case \"array\":\n if (ir.checks?.some((c) => \"refIndex\" in c && c.refIndex !== undefined)) return true;\n break;\n default:\n break;\n }\n return childSchemas(ir).some(referencesExportRefs);\n}\n\n// ─── Candidate collection ────────────────────────────────────────────────────\n\nfunction collect(\n ir: SchemaIR,\n keyMemo: Map<SchemaIR, string>,\n weightMemo: Map<SchemaIR, number>,\n stats: Map<string, { ir: SchemaIR; count: number; weight: number }>,\n): void {\n const key = keyOf(ir, keyMemo);\n const existing = stats.get(key);\n if (existing) {\n existing.count++;\n } else {\n stats.set(key, { ir, count: 1, weight: weightOf(ir, weightMemo) });\n }\n for (const child of childSchemas(ir)) collect(child, keyMemo, weightMemo, stats);\n}\n\n/** Direct child SchemaIR nodes (the structural recursion shared by every pass). */\nfunction childSchemas(ir: SchemaIR): SchemaIR[] {\n switch (ir.type) {\n case \"object\":\n return Object.values(ir.properties);\n case \"array\":\n return [ir.element];\n case \"tuple\":\n return ir.rest === null ? ir.items : [...ir.items, ir.rest];\n case \"record\":\n case \"map\":\n return [ir.keyType, ir.valueType];\n case \"set\":\n return [ir.valueType];\n case \"union\":\n case \"discriminatedUnion\":\n return ir.options;\n case \"intersection\":\n return [ir.left, ir.right];\n case \"optional\":\n case \"nullable\":\n case \"readonly\":\n case \"default\":\n case \"catch\":\n return [ir.inner];\n case \"pipe\":\n return [ir.in, ir.out];\n case \"effect\":\n case \"recursionTarget\":\n return [ir.inner];\n default:\n return [];\n }\n}\n\n/** Subtree weight ≈ node count plus one per check; memoized for O(total) cost. */\nfunction weightOf(ir: SchemaIR, memo: Map<SchemaIR, number>): number {\n const hit = memo.get(ir);\n if (hit !== undefined) return hit;\n const checks = (ir as { checks?: readonly unknown[] }).checks;\n let weight = 1 + (Array.isArray(checks) ? checks.length : 0);\n for (const child of childSchemas(ir)) weight += weightOf(child, memo);\n memo.set(ir, weight);\n return weight;\n}\n\nfunction containsRecursiveRef(ir: SchemaIR): boolean {\n if (ir.type === \"recursiveRef\") return true;\n return childSchemas(ir).some(containsRecursiveRef);\n}\n\n// ─── Structural keys ─────────────────────────────────────────────────────────\n\n/**\n * Stable structural key for an IR node, memoized by node identity so each\n * subtree is serialized exactly once (the whole pass is O(total nodes), not\n * O(nodes × depth)). Two nodes share a key iff they are structurally identical\n * — including every check, literal value, and error message — so deduplicated\n * schemas produce byte-identical validation and issues.\n */\nfunction keyOf(ir: SchemaIR, memo: Map<SchemaIR, string>): string {\n const hit = memo.get(ir);\n if (hit !== undefined) return hit;\n const obj = ir as unknown as Record<string, unknown>;\n const key = `{${Object.keys(obj)\n .filter((k) => obj[k] !== undefined)\n .sort()\n .map((k) => `${k}:${encodeValue(obj[k], memo)}`)\n .join(\",\")}}`;\n memo.set(ir, key);\n return key;\n}\n\n/**\n * Encode any IR field value. A nested SchemaIR is detected by a string `type`\n * field (checks carry `kind`, union `cases` carry `value`/`option` — only real\n * schema nodes have `type`) and routed through {@link keyOf} for subtree\n * memoization; everything else is serialized in sorted-key order.\n */\nfunction encodeValue(value: unknown, memo: Map<SchemaIR, string>): string {\n if (typeof value === \"bigint\") return `#${value}`;\n if (value === null) return \"null\";\n // Non-finite numbers must stay distinct: JSON.stringify maps Infinity, -Infinity\n // and NaN all to \"null\", which would merge e.g. .min(Infinity) with .min(NaN).\n if (typeof value === \"number\" && !Number.isFinite(value)) return `n!${value}`;\n if (Array.isArray(value)) return `[${value.map((v) => encodeValue(v, memo)).join(\",\")}]`;\n if (typeof value === \"object\") {\n if (typeof (value as { type?: unknown }).type === \"string\") {\n return keyOf(value as SchemaIR, memo);\n }\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .filter((k) => obj[k] !== undefined)\n .sort()\n .map((k) => `${k}:${encodeValue(obj[k], memo)}`)\n .join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"undefined\";\n}\n\nfunction cmp(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dedupe.js","names":[],"sources":["../../../src/core/codegen/dedupe.ts"],"sourcesContent":["/**\n * File-level schema deduplication.\n *\n * When several schemas in one file share structurally identical sub-trees\n * (a reused `Address` object, a `{ id, name }` row, an exported schema that is\n * also embedded in another), the error-collecting \"slow walk\" for that shape\n * is generated ONCE as a shared `__zcSw_N(input, path, issues)` function and\n * every occurrence calls it instead of inlining a fresh copy.\n *\n * Two invariants keep this free of runtime cost and footguns:\n *\n * 1. **Slow walk only.** The fast path (the `&&` boolean chain that validates\n * valid input with zero allocations) is NEVER shared — it stays fully\n * inlined so the hot path is byte-for-byte what it was before. Only the\n * cold, bulky error walk (64–77% of generated bytes) is shared.\n *\n * 2. **No per-export references.** A shared body lives at MODULE scope, while\n * `__rf[]` — the array of retained Zod schemas and captured callbacks — is\n * declared inside each export's IIFE. So a shape may be shared only when\n * nothing in it resolves through `__rf`: no `default`, `catch`, `fallback`,\n * captured `transform`, or captured `refine`/`superRefine`. Zero-capture\n * callbacks are fine — they are hosted from source text into the shared\n * block's own preamble.\n *\n * Sharing used to additionally require the whole ROOT to be mutation-free, so\n * that a body which wrote nothing back could not lose an output value. That\n * excluded a genuine `z.object()` — it strips, so its walk PRODUCES a rebuilt\n * object — which, once `z.object()` began always stripping, meant the feature\n * stopped firing for essentially every real schema: a realistic module of four\n * schemas reusing two sub-shapes shared nothing and shipped 29.6 KB where the\n * `strictObject` spelling of the same thing shipped 16.7 KB.\n *\n * The write-back problem is solved directly instead: a shared walk RETURNS its\n * value and call sites assign it (`slot = __zcSw_N(slot, path, issues)`), so a\n * stripping object, a coercion or a `.trim()` delivers its rewritten value\n * through the call exactly as an inlined walk delivers it through `g.output`.\n */\n\nimport type { SchemaIR } from \"../types.js\";\nimport type { CodeGenContext, CodegenMode } from \"./context.js\";\nimport { createSlowGen, generateSlow } from \"./slow-path.js\";\n\n/** A shared slow-walk validator hosted at module scope. */\nexport interface SharedSchemaRef {\n /** Name of the `function __zcSw_N(input, path, issues)` declaration. */\n readonly name: string;\n}\n\nexport interface SharedSchemaPlan {\n /**\n * Shared ref for an IR node, or undefined when the node is not a shared\n * candidate. Keyed by structural identity (memoized), so repeated lookups\n * during codegen are O(1).\n */\n refFor(ir: SchemaIR): SharedSchemaRef | undefined;\n /** Module-scope declarations (shared preamble + `__zcSw_N` functions). `\"\"` when empty. */\n readonly code: string;\n /** Runtime helper names referenced by the shared block (lean mode). */\n readonly usedHelpers: Set<string>;\n readonly isEmpty: boolean;\n}\n\n/**\n * Minimum subtree weight (≈ node count, +1 per check) for a repeated shape to\n * be worth a shared function. Below this the call + dynamic-path overhead\n * outweighs the duplicated inline walk; ~4 admits a 3-field object or an\n * array-of-object while skipping bare primitives and thin wrappers.\n */\nconst MIN_SHARED_WEIGHT = 4;\n\n/** Leading marker of a shared block; used to skip re-injection on watch/HMR re-runs. */\nexport const SHARED_BLOCK_MARKER = \"/* zod-compiler shared */\";\n\nconst EMPTY_PLAN: SharedSchemaPlan = {\n refFor: () => undefined,\n code: \"\",\n usedHelpers: new Set(),\n isEmpty: true,\n};\n\n/**\n * Build the shared-schema plan for one file's root schemas. Returns\n * {@link EMPTY_PLAN} (a no-op) when nothing repeats — the common case, so files\n * without internal duplication pay only a single linear analysis pass.\n */\nexport function createSharedSchemaPlan(\n roots: readonly SchemaIR[],\n mode: CodegenMode,\n): SharedSchemaPlan {\n const keyMemo = new Map<SchemaIR, string>();\n const weightMemo = new Map<SchemaIR, number>();\n const stats = new Map<string, { ir: SchemaIR; count: number; weight: number }>();\n\n for (const root of roots) {\n collect(root, keyMemo, weightMemo, stats);\n }\n\n const selected = [...stats.values()]\n .filter(\n (c) =>\n c.count >= 2 &&\n c.weight >= MIN_SHARED_WEIGHT &&\n !containsRecursiveRef(c.ir) &&\n !referencesExportRefs(c.ir),\n )\n // Largest shapes first so the biggest wins get the lowest indices; tie-break\n // on the structural key for deterministic, reproducible output.\n .sort((a, b) => b.weight - a.weight || cmp(keyOf(a.ir, keyMemo), keyOf(b.ir, keyMemo)));\n\n if (selected.length === 0) return EMPTY_PLAN;\n\n const refs = new Map<string, SharedSchemaRef>();\n selected.forEach((c, i) => refs.set(keyOf(c.ir, keyMemo), { name: `__zcSw_${i}` }));\n\n const refFor = (ir: SchemaIR): SharedSchemaRef | undefined => refs.get(keyOf(ir, keyMemo));\n\n const ctx: CodeGenContext = {\n preamble: [],\n counter: 0,\n fnName: \"__zcShared\",\n regexCache: new Map(),\n mode,\n usedHelpers: new Set(),\n };\n // Generation consults the plan so a shared candidate nested inside another\n // shared candidate becomes a `__zcSw_*` call rather than a second inline copy.\n // Containment is strictly size-decreasing, so the shared functions form a DAG;\n // hoisted declarations make call order irrelevant.\n ctx.sharedSchemas = { refFor, code: \"\", usedHelpers: ctx.usedHelpers, isEmpty: false };\n\n const fns: string[] = [];\n for (const c of selected) {\n const ref = refs.get(keyOf(c.ir, keyMemo));\n if (ref === undefined) continue;\n // Generate the candidate's body INLINE (the root is passed to generateSlow,\n // not visit(), so it never replaces itself with a call); only its children\n // route through visit() and may resolve to other shared refs.\n const body = generateSlow(c.ir, createSlowGen(\"input\", \"input\", \"path\", \"_e\", ctx));\n // `input` is both the read and the write slot (createSlowGen above passes it\n // as input AND output), so a walk that rewrites — a stripping object, a\n // coercion, an overwrite check — leaves its result there. Returning it is\n // what lets the caller receive that value; a walk that rewrites nothing\n // returns its argument unchanged.\n fns.push(`function ${ref.name}(input,path,_e){\\n${body}\\nreturn input;\\n}`);\n }\n\n return {\n refFor,\n code: [SHARED_BLOCK_MARKER, ...ctx.preamble, ...fns].join(\"\\n\"),\n usedHelpers: ctx.usedHelpers,\n isEmpty: false,\n };\n}\n\n// A shared sub-schema is called as `slot = __zcSw_N(slot, path, issues)`: the\n// walk pushes directly into the caller's `issues` array (no result object, no\n// per-call issue copy) and returns the parsed value, which the call site assigns\n// back into the slot the inlined walk would have written. Call sites are emitted\n// by slow-path.ts (nested occurrences) and index.ts (a shared root), keyed off\n// `ref.name`.\n\n/**\n * Does the subtree resolve anything through the per-export `__rf[]` array —\n * a retained Zod schema (`default`, `catch`, `fallback`) or a captured callback\n * (`transform`, `refine`, `superRefine`)?\n *\n * Those names exist only inside an export's own IIFE, so a module-scope shared\n * body referencing them would be unbound. A zero-capture callback carries\n * `source` instead of `refIndex` and is hosted into the shared block's preamble,\n * so it does not disqualify a shape.\n */\nfunction referencesExportRefs(ir: SchemaIR): boolean {\n switch (ir.type) {\n case \"default\":\n case \"catch\":\n case \"fallback\":\n // `refIndex` is mandatory on these — the value/schema lives on the export.\n return true;\n case \"custom\":\n // Both the predicate and pristine error delegate live on the export.\n return true;\n case \"zodDelegate\":\n return true;\n case \"effect\":\n if (ir.refIndex !== undefined) return true;\n break;\n case \"string\":\n case \"number\":\n case \"object\":\n case \"array\":\n if (ir.checks?.some((c) => \"refIndex\" in c && c.refIndex !== undefined)) return true;\n break;\n default:\n break;\n }\n return childSchemas(ir).some(referencesExportRefs);\n}\n\n// ─── Candidate collection ────────────────────────────────────────────────────\n\nfunction collect(\n ir: SchemaIR,\n keyMemo: Map<SchemaIR, string>,\n weightMemo: Map<SchemaIR, number>,\n stats: Map<string, { ir: SchemaIR; count: number; weight: number }>,\n): void {\n const key = keyOf(ir, keyMemo);\n const existing = stats.get(key);\n if (existing) {\n existing.count++;\n } else {\n stats.set(key, { ir, count: 1, weight: weightOf(ir, weightMemo) });\n }\n for (const child of childSchemas(ir)) collect(child, keyMemo, weightMemo, stats);\n}\n\n/** Direct child SchemaIR nodes (the structural recursion shared by every pass). */\nfunction childSchemas(ir: SchemaIR): SchemaIR[] {\n switch (ir.type) {\n case \"object\":\n return Object.values(ir.properties);\n case \"array\":\n return [ir.element];\n case \"tuple\":\n return ir.rest === null ? ir.items : [...ir.items, ir.rest];\n case \"record\":\n case \"map\":\n return [ir.keyType, ir.valueType];\n case \"set\":\n return [ir.valueType];\n case \"union\":\n case \"discriminatedUnion\":\n return ir.options;\n case \"intersection\":\n return [ir.left, ir.right];\n case \"optional\":\n case \"nullable\":\n case \"readonly\":\n case \"default\":\n case \"catch\":\n return [ir.inner];\n case \"pipe\":\n return [ir.in, ir.out];\n case \"effect\":\n case \"recursionTarget\":\n case \"zodDelegate\":\n return [ir.inner];\n default:\n return [];\n }\n}\n\n/** Subtree weight ≈ node count plus one per check; memoized for O(total) cost. */\nfunction weightOf(ir: SchemaIR, memo: Map<SchemaIR, number>): number {\n const hit = memo.get(ir);\n if (hit !== undefined) return hit;\n const checks = (ir as { checks?: readonly unknown[] }).checks;\n let weight = 1 + (Array.isArray(checks) ? checks.length : 0);\n for (const child of childSchemas(ir)) weight += weightOf(child, memo);\n memo.set(ir, weight);\n return weight;\n}\n\nfunction containsRecursiveRef(ir: SchemaIR): boolean {\n if (ir.type === \"recursiveRef\") return true;\n return childSchemas(ir).some(containsRecursiveRef);\n}\n\n// ─── Structural keys ─────────────────────────────────────────────────────────\n\n/**\n * Stable structural key for an IR node, memoized by node identity so each\n * subtree is serialized exactly once (the whole pass is O(total nodes), not\n * O(nodes × depth)). Two nodes share a key iff they are structurally identical\n * — including every check, literal value, and error message — so deduplicated\n * schemas produce byte-identical validation and issues.\n */\nfunction keyOf(ir: SchemaIR, memo: Map<SchemaIR, string>): string {\n const hit = memo.get(ir);\n if (hit !== undefined) return hit;\n const obj = ir as unknown as Record<string, unknown>;\n const key = `{${Object.keys(obj)\n .filter((k) => obj[k] !== undefined)\n .sort()\n .map((k) => `${k}:${encodeValue(obj[k], memo)}`)\n .join(\",\")}}`;\n memo.set(ir, key);\n return key;\n}\n\n/**\n * Encode any IR field value. A nested SchemaIR is detected by a string `type`\n * field (checks carry `kind`, union `cases` carry `value`/`option` — only real\n * schema nodes have `type`) and routed through {@link keyOf} for subtree\n * memoization; everything else is serialized in sorted-key order.\n */\nfunction encodeValue(value: unknown, memo: Map<SchemaIR, string>): string {\n if (typeof value === \"bigint\") return `#${value}`;\n if (value === null) return \"null\";\n // Non-finite numbers must stay distinct: JSON.stringify maps Infinity, -Infinity\n // and NaN all to \"null\", which would merge e.g. .min(Infinity) with .min(NaN).\n if (typeof value === \"number\" && !Number.isFinite(value)) return `n!${value}`;\n if (Array.isArray(value)) return `[${value.map((v) => encodeValue(v, memo)).join(\",\")}]`;\n if (typeof value === \"object\") {\n if (typeof (value as { type?: unknown }).type === \"string\") {\n return keyOf(value as SchemaIR, memo);\n }\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .filter((k) => obj[k] !== undefined)\n .sort()\n .map((k) => `${k}:${encodeValue(obj[k], memo)}`)\n .join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"undefined\";\n}\n\nfunction cmp(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fast-path.d.ts","names":[],"sources":["../../../src/core/codegen/fast-path.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"fast-path.d.ts","names":[],"sources":["../../../src/core/codegen/fast-path.ts"],"mappings":";;;;;;;;;;;;;iBA0HgB,cACd,mBACA,KAAK,gBACL,uBACA,QAAO,WACP,uBACC;;;;;;;;;;;;;;;;;iBA8Ca,aAAa,IAAI,UAAU,GAAG"}
|
|
@@ -34,6 +34,7 @@ import { fastUndefined } from "./schemas/undefined.js";
|
|
|
34
34
|
import { fastUnion } from "./schemas/union.js";
|
|
35
35
|
import { fastUnknown } from "./schemas/unknown.js";
|
|
36
36
|
import { fastVoid } from "./schemas/void.js";
|
|
37
|
+
import { fastZodDelegate } from "./schemas/zod-delegate.js";
|
|
37
38
|
//#region src/core/codegen/fast-path.ts
|
|
38
39
|
const fastRegistry = {
|
|
39
40
|
string: fastString,
|
|
@@ -68,6 +69,7 @@ const fastRegistry = {
|
|
|
68
69
|
pipe: fastPipe,
|
|
69
70
|
effect: null,
|
|
70
71
|
custom: fastCustom,
|
|
72
|
+
zodDelegate: fastZodDelegate,
|
|
71
73
|
templateLiteral: fastTemplateLiteral,
|
|
72
74
|
catch: fastCatch,
|
|
73
75
|
fallback: null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fast-path.js","names":[],"sources":["../../../src/core/codegen/fast-path.ts"],"sourcesContent":["/**\n * Fast Path: produces a boolean expression string for eligible schemas.\n * When the expression evaluates to `true` at runtime, the input is valid and\n * safeParse can return success immediately without allocating an issues array.\n *\n * Returns `null` if the schema is not eligible for Fast Path (contains coerce,\n * default, transform, or other non-pure constructs).\n */\n\nimport type { SchemaIR } from \"../types.js\";\nimport type { CodeGenContext, FastGen, FastGenerator, FastScope } from \"./context.js\";\nimport { declareFastTemps, emitRegex, emitTemp } from \"./context.js\";\nimport {\n CALL_COST,\n EXTRACT_CAP,\n estimateFastCost,\n HOISTABLE,\n MIN_EXTRACT,\n predictedInlineSize,\n} from \"./fast-size.js\";\nimport { fastAny } from \"./schemas/any.js\";\nimport { fastArray } from \"./schemas/array.js\";\nimport { fastBigInt } from \"./schemas/bigint.js\";\nimport { fastBoolean } from \"./schemas/boolean.js\";\nimport { fastCatch } from \"./schemas/catch.js\";\nimport { fastCustom } from \"./schemas/custom.js\";\nimport { fastDate } from \"./schemas/date.js\";\nimport { fastDefault } from \"./schemas/default.js\";\nimport { fastDiscriminatedUnion } from \"./schemas/discriminated-union.js\";\nimport { fastEnum } from \"./schemas/enum.js\";\nimport { fastFile } from \"./schemas/file.js\";\nimport { fastIntersection } from \"./schemas/intersection.js\";\nimport { fastLiteral } from \"./schemas/literal.js\";\nimport { fastMap } from \"./schemas/map.js\";\nimport { fastNan } from \"./schemas/nan.js\";\nimport { fastNever } from \"./schemas/never.js\";\nimport { fastNull } from \"./schemas/null.js\";\nimport { fastNullable } from \"./schemas/nullable.js\";\nimport { fastNumber } from \"./schemas/number.js\";\nimport { fastObject } from \"./schemas/object.js\";\nimport { fastOptional } from \"./schemas/optional.js\";\nimport { fastPipe } from \"./schemas/pipe.js\";\nimport { fastReadonly } from \"./schemas/readonly.js\";\nimport { fastRecord } from \"./schemas/record.js\";\nimport { fastRecursionTarget, fastRecursiveRef } from \"./schemas/recursive-ref.js\";\nimport { fastSet } from \"./schemas/set.js\";\nimport { fastString } from \"./schemas/string.js\";\nimport { fastSymbol } from \"./schemas/symbol.js\";\nimport { fastTemplateLiteral } from \"./schemas/template-literal.js\";\nimport { fastTuple } from \"./schemas/tuple.js\";\nimport { fastUndefined } from \"./schemas/undefined.js\";\nimport { fastUnion } from \"./schemas/union.js\";\nimport { fastUnknown } from \"./schemas/unknown.js\";\nimport { fastVoid } from \"./schemas/void.js\";\n\n// ─── Typed registry ─────────────────────────────────────────────────────────\n// `null` = statically ineligible (the type NEVER has a fast path).\n// Non-null function = may return null at runtime (dynamically ineligible, e.g. when coerce is present).\n\nconst fastRegistry = {\n // Primitives (order follows SchemaIR union in types.ts)\n string: fastString,\n number: fastNumber,\n boolean: fastBoolean,\n bigint: fastBigInt,\n date: fastDate,\n symbol: fastSymbol,\n null: fastNull,\n undefined: fastUndefined,\n void: fastVoid,\n nan: fastNan,\n never: fastNever,\n any: fastAny,\n unknown: fastUnknown,\n literal: fastLiteral,\n enum: fastEnum,\n // Containers\n object: fastObject,\n array: fastArray,\n tuple: fastTuple,\n record: fastRecord,\n set: fastSet,\n map: fastMap,\n file: fastFile,\n // Unions & Intersections\n union: fastUnion,\n discriminatedUnion: fastDiscriminatedUnion,\n intersection: fastIntersection,\n // Modifiers\n optional: fastOptional,\n nullable: fastNullable,\n readonly: fastReadonly,\n default: fastDefault,\n pipe: fastPipe,\n // Effects\n effect: null, // statically ineligible\n // Special\n custom: fastCustom,\n templateLiteral: fastTemplateLiteral,\n catch: fastCatch,\n fallback: null, // statically ineligible\n recursiveRef: fastRecursiveRef,\n recursionTarget: fastRecursionTarget,\n stringBool: null, // statically ineligible — output type (boolean) differs from input (string)\n} satisfies {\n [K in SchemaIR[\"type\"]]: FastGenerator<Extract<SchemaIR, { type: K }>> | null;\n};\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * @param inputExpr the input expression this node validates\n * @param ctx shared codegen context\n * @param extractable whether THIS node may be hoisted (false for the root and a\n * helper's own top node — see generateFast); children are always extractable\n * @param scope size accumulator for the function being assembled (a child\n * visit shares the parent's scope; a hoisted helper body gets a fresh one)\n * @param discSkipKey discriminated-union option marker (see FastGen.discSkipKey);\n * carried only on the option's own node, never auto-propagated to children\n */\nexport function createFastGen(\n inputExpr: string,\n ctx: CodeGenContext,\n extractable = false,\n scope: FastScope = { temps: [], used: 0 },\n discSkipKey?: string,\n): FastGen {\n return {\n input: inputExpr,\n ctx,\n extractable,\n scope,\n discSkipKey,\n visit(ir, overrides) {\n // A child shares this function's scope and is itself extractable. The\n // discriminated-union skip is NOT inherited implicitly — only a caller\n // that explicitly passes overrides.discSkipKey (fastDiscriminatedUnion)\n // sets it, so nested objects keep their own type-guard.\n return generateFast(\n ir,\n createFastGen(overrides?.input ?? inputExpr, ctx, true, scope, overrides?.discSkipKey),\n );\n },\n scoped: (input) => createFastGen(input, ctx, true, { temps: [], used: 0 }),\n temp: (prefix) => emitTemp(ctx, prefix),\n local(prefix) {\n const name = emitTemp(ctx, prefix);\n scope.temps.push(name);\n return name;\n },\n regex: (prefix, pattern, flags) => emitRegex(ctx, prefix, pattern, flags),\n };\n}\n\n// ─── Dispatch ────────────────────────────────────────────────────────────────\n\n/**\n * Generate a boolean expression string that validates input against the schema.\n * Returns `null` if the schema (or any nested part) is not eligible for fast checking.\n *\n * Size-gated extraction: when inlining `ir` would push the function being\n * assembled past EXTRACT_CAP, it is hoisted into its own boolean helper\n * `function __fo_N(p){return <expr over p>;}` and replaced by a call. This keeps\n * every emitted function under V8's TurboFan optimization budget — a single\n * giant fast-check otherwise drops to the slower Maglev tier (measured ~3-4.5x\n * on deeply-nested schemas). The running total `g.scope.used` is EXACT emitted\n * chars (the returned string's length); only the extract look-ahead is an\n * estimate, scaled for access-path length. Small schemas never approach the\n * cap, so their output is byte-identical to the fully-inlined form; large\n * aggregates do split (still semantically identical — only the boolean's shape\n * changes).\n */\nexport function generateFast(ir: SchemaIR, g: FastGen): string | null {\n const gen = fastRegistry[ir.type];\n if (gen === null) return null;\n\n if (g.extractable && HOISTABLE.has(ir.type)) {\n const cache = fastSizeCache(g.ctx);\n const overBudget = g.scope.used + predictedInlineSize(ir, g.input.length, cache) > EXTRACT_CAP;\n // Extract a sub-schema big enough to be worth a call; but once the function\n // is ALREADY over budget, extract any hoistable child regardless of size —\n // otherwise a crowd of small siblings (e.g. a discriminated union of many\n // small options) grows the function unbounded since none alone clears\n // MIN_EXTRACT.\n if (overBudget && (g.scope.used > EXTRACT_CAP || estimateFastCost(ir, cache) >= MIN_EXTRACT)) {\n const param = emitTemp(g.ctx, \"op\");\n // Generate the body relative to the helper's parameter, in a fresh scope;\n // the top node is non-extractable (it IS this helper) while its children\n // stay extractable, so an oversized helper splits further recursively.\n // Carry discSkipKey so an extracted discriminated-union option still drops\n // its redundant guard inside the hoisted helper.\n const innerGen = createFastGen(param, g.ctx, false, { temps: [], used: 0 }, g.discSkipKey);\n const inner = generateFast(ir, innerGen);\n if (inner === null) return null; // ineligible sub-schema disables the whole fast path\n const fnName = emitTemp(g.ctx, \"fo\");\n g.ctx.preamble.push(\n `function ${fnName}(${param}){${declareFastTemps(innerGen.scope)}return ${inner};}`,\n );\n g.scope.used += CALL_COST;\n return `${fnName}(${g.input})`;\n }\n }\n\n // Inlined: charge the enclosing function this node's EXACT emitted size. The\n // node's own text plus its inlined descendants is exactly the returned string;\n // children mutated the shared scope as they were visited, but the parent's\n // length is authoritative, so overwrite rather than add.\n const before = g.scope.used;\n // oxlint-disable-next-line typescript/no-explicit-any -- registry dispatch requires type erasure at call site\n const out = (gen as any)(ir, g) as string | null;\n if (out === null) return null;\n g.scope.used = before + out.length;\n return out;\n}\n\n/** Per-compile memo for estimateFastCost, stashed on the shared context. */\nfunction fastSizeCache(ctx: CodeGenContext): WeakMap<SchemaIR, number> {\n return (ctx.fastSizeCache ??= new WeakMap<SchemaIR, number>());\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAM,eAAe;CAEnB,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,MAAM;CACN,WAAW;CACX,MAAM;CACN,KAAK;CACL,OAAO;CACP,KAAK;CACL,SAAS;CACT,SAAS;CACT,MAAM;CAEN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,QAAQ;CACR,KAAK;CACL,KAAK;CACL,MAAM;CAEN,OAAO;CACP,oBAAoB;CACpB,cAAc;CAEd,UAAU;CACV,UAAU;CACV,UAAU;CACV,SAAS;CACT,MAAM;CAEN,QAAQ;CAER,QAAQ;CACR,iBAAiB;CACjB,OAAO;CACP,UAAU;CACV,cAAc;CACd,iBAAiB;CACjB,YAAY;AACd;;;;;;;;;;;AAgBA,SAAgB,cACd,WACA,KACA,cAAc,OACd,QAAmB;CAAE,OAAO,CAAC;CAAG,MAAM;AAAE,GACxC,aACS;CACT,OAAO;EACL,OAAO;EACP;EACA;EACA;EACA;EACA,MAAM,IAAI,WAAW;GAKnB,OAAO,aACL,IACA,cAAc,WAAW,SAAS,WAAW,KAAK,MAAM,OAAO,WAAW,WAAW,CACvF;EACF;EACA,SAAS,UAAU,cAAc,OAAO,KAAK,MAAM;GAAE,OAAO,CAAC;GAAG,MAAM;EAAE,CAAC;EACzE,OAAO,WAAW,SAAS,KAAK,MAAM;EACtC,MAAM,QAAQ;GACZ,MAAM,OAAO,SAAS,KAAK,MAAM;GACjC,MAAM,MAAM,KAAK,IAAI;GACrB,OAAO;EACT;EACA,QAAQ,QAAQ,SAAS,UAAU,UAAU,KAAK,QAAQ,SAAS,KAAK;CAC1E;AACF;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAa,IAAc,GAA2B;CACpE,MAAM,MAAM,aAAa,GAAG;CAC5B,IAAI,QAAQ,MAAM,OAAO;CAEzB,IAAI,EAAE,eAAe,UAAU,IAAI,GAAG,IAAI,GAAG;EAC3C,MAAM,QAAQ,cAAc,EAAE,GAAG;EAOjC,IANmB,EAAE,MAAM,OAAO,oBAAoB,IAAI,EAAE,MAAM,QAAQ,KAAK,IAAA,SAM5D,EAAE,MAAM,OAAA,QAAsB,iBAAiB,IAAI,KAAK,KAAA,OAAmB;GAC5F,MAAM,QAAQ,SAAS,EAAE,KAAK,IAAI;GAMlC,MAAM,WAAW,cAAc,OAAO,EAAE,KAAK,OAAO;IAAE,OAAO,CAAC;IAAG,MAAM;GAAE,GAAG,EAAE,WAAW;GACzF,MAAM,QAAQ,aAAa,IAAI,QAAQ;GACvC,IAAI,UAAU,MAAM,OAAO;GAC3B,MAAM,SAAS,SAAS,EAAE,KAAK,IAAI;GACnC,EAAE,IAAI,SAAS,KACb,YAAY,OAAO,GAAG,MAAM,IAAI,iBAAiB,SAAS,KAAK,EAAE,SAAS,MAAM,GAClF;GACA,EAAE,MAAM,QAAA;GACR,OAAO,GAAG,OAAO,GAAG,EAAE,MAAM;EAC9B;CACF;CAMA,MAAM,SAAS,EAAE,MAAM;CAEvB,MAAM,MAAO,IAAY,IAAI,CAAC;CAC9B,IAAI,QAAQ,MAAM,OAAO;CACzB,EAAE,MAAM,OAAO,SAAS,IAAI;CAC5B,OAAO;AACT;;AAGA,SAAS,cAAc,KAAgD;CACrE,OAAQ,IAAI,kCAAkB,IAAI,QAA0B;AAC9D"}
|
|
1
|
+
{"version":3,"file":"fast-path.js","names":[],"sources":["../../../src/core/codegen/fast-path.ts"],"sourcesContent":["/**\n * Fast Path: produces a boolean expression string for eligible schemas.\n * When the expression evaluates to `true` at runtime, the input is valid and\n * safeParse can return success immediately without allocating an issues array.\n *\n * Returns `null` if the schema is not eligible for Fast Path (contains coerce,\n * default, transform, or other non-pure constructs).\n */\n\nimport type { SchemaIR } from \"../types.js\";\nimport type { CodeGenContext, FastGen, FastGenerator, FastScope } from \"./context.js\";\nimport { declareFastTemps, emitRegex, emitTemp } from \"./context.js\";\nimport {\n CALL_COST,\n EXTRACT_CAP,\n estimateFastCost,\n HOISTABLE,\n MIN_EXTRACT,\n predictedInlineSize,\n} from \"./fast-size.js\";\nimport { fastAny } from \"./schemas/any.js\";\nimport { fastArray } from \"./schemas/array.js\";\nimport { fastBigInt } from \"./schemas/bigint.js\";\nimport { fastBoolean } from \"./schemas/boolean.js\";\nimport { fastCatch } from \"./schemas/catch.js\";\nimport { fastCustom } from \"./schemas/custom.js\";\nimport { fastDate } from \"./schemas/date.js\";\nimport { fastDefault } from \"./schemas/default.js\";\nimport { fastDiscriminatedUnion } from \"./schemas/discriminated-union.js\";\nimport { fastEnum } from \"./schemas/enum.js\";\nimport { fastFile } from \"./schemas/file.js\";\nimport { fastIntersection } from \"./schemas/intersection.js\";\nimport { fastLiteral } from \"./schemas/literal.js\";\nimport { fastMap } from \"./schemas/map.js\";\nimport { fastNan } from \"./schemas/nan.js\";\nimport { fastNever } from \"./schemas/never.js\";\nimport { fastNull } from \"./schemas/null.js\";\nimport { fastNullable } from \"./schemas/nullable.js\";\nimport { fastNumber } from \"./schemas/number.js\";\nimport { fastObject } from \"./schemas/object.js\";\nimport { fastOptional } from \"./schemas/optional.js\";\nimport { fastPipe } from \"./schemas/pipe.js\";\nimport { fastReadonly } from \"./schemas/readonly.js\";\nimport { fastRecord } from \"./schemas/record.js\";\nimport { fastRecursionTarget, fastRecursiveRef } from \"./schemas/recursive-ref.js\";\nimport { fastSet } from \"./schemas/set.js\";\nimport { fastString } from \"./schemas/string.js\";\nimport { fastSymbol } from \"./schemas/symbol.js\";\nimport { fastTemplateLiteral } from \"./schemas/template-literal.js\";\nimport { fastTuple } from \"./schemas/tuple.js\";\nimport { fastUndefined } from \"./schemas/undefined.js\";\nimport { fastUnion } from \"./schemas/union.js\";\nimport { fastUnknown } from \"./schemas/unknown.js\";\nimport { fastVoid } from \"./schemas/void.js\";\nimport { fastZodDelegate } from \"./schemas/zod-delegate.js\";\n\n// ─── Typed registry ─────────────────────────────────────────────────────────\n// `null` = statically ineligible (the type NEVER has a fast path).\n// Non-null function = may return null at runtime (dynamically ineligible, e.g. when coerce is present).\n\nconst fastRegistry = {\n // Primitives (order follows SchemaIR union in types.ts)\n string: fastString,\n number: fastNumber,\n boolean: fastBoolean,\n bigint: fastBigInt,\n date: fastDate,\n symbol: fastSymbol,\n null: fastNull,\n undefined: fastUndefined,\n void: fastVoid,\n nan: fastNan,\n never: fastNever,\n any: fastAny,\n unknown: fastUnknown,\n literal: fastLiteral,\n enum: fastEnum,\n // Containers\n object: fastObject,\n array: fastArray,\n tuple: fastTuple,\n record: fastRecord,\n set: fastSet,\n map: fastMap,\n file: fastFile,\n // Unions & Intersections\n union: fastUnion,\n discriminatedUnion: fastDiscriminatedUnion,\n intersection: fastIntersection,\n // Modifiers\n optional: fastOptional,\n nullable: fastNullable,\n readonly: fastReadonly,\n default: fastDefault,\n pipe: fastPipe,\n // Effects\n effect: null, // statically ineligible\n // Special\n custom: fastCustom,\n zodDelegate: fastZodDelegate,\n templateLiteral: fastTemplateLiteral,\n catch: fastCatch,\n fallback: null, // statically ineligible\n recursiveRef: fastRecursiveRef,\n recursionTarget: fastRecursionTarget,\n stringBool: null, // statically ineligible — output type (boolean) differs from input (string)\n} satisfies {\n [K in SchemaIR[\"type\"]]: FastGenerator<Extract<SchemaIR, { type: K }>> | null;\n};\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/**\n * @param inputExpr the input expression this node validates\n * @param ctx shared codegen context\n * @param extractable whether THIS node may be hoisted (false for the root and a\n * helper's own top node — see generateFast); children are always extractable\n * @param scope size accumulator for the function being assembled (a child\n * visit shares the parent's scope; a hoisted helper body gets a fresh one)\n * @param discSkipKey discriminated-union option marker (see FastGen.discSkipKey);\n * carried only on the option's own node, never auto-propagated to children\n */\nexport function createFastGen(\n inputExpr: string,\n ctx: CodeGenContext,\n extractable = false,\n scope: FastScope = { temps: [], used: 0 },\n discSkipKey?: string,\n): FastGen {\n return {\n input: inputExpr,\n ctx,\n extractable,\n scope,\n discSkipKey,\n visit(ir, overrides) {\n // A child shares this function's scope and is itself extractable. The\n // discriminated-union skip is NOT inherited implicitly — only a caller\n // that explicitly passes overrides.discSkipKey (fastDiscriminatedUnion)\n // sets it, so nested objects keep their own type-guard.\n return generateFast(\n ir,\n createFastGen(overrides?.input ?? inputExpr, ctx, true, scope, overrides?.discSkipKey),\n );\n },\n scoped: (input) => createFastGen(input, ctx, true, { temps: [], used: 0 }),\n temp: (prefix) => emitTemp(ctx, prefix),\n local(prefix) {\n const name = emitTemp(ctx, prefix);\n scope.temps.push(name);\n return name;\n },\n regex: (prefix, pattern, flags) => emitRegex(ctx, prefix, pattern, flags),\n };\n}\n\n// ─── Dispatch ────────────────────────────────────────────────────────────────\n\n/**\n * Generate a boolean expression string that validates input against the schema.\n * Returns `null` if the schema (or any nested part) is not eligible for fast checking.\n *\n * Size-gated extraction: when inlining `ir` would push the function being\n * assembled past EXTRACT_CAP, it is hoisted into its own boolean helper\n * `function __fo_N(p){return <expr over p>;}` and replaced by a call. This keeps\n * every emitted function under V8's TurboFan optimization budget — a single\n * giant fast-check otherwise drops to the slower Maglev tier (measured ~3-4.5x\n * on deeply-nested schemas). The running total `g.scope.used` is EXACT emitted\n * chars (the returned string's length); only the extract look-ahead is an\n * estimate, scaled for access-path length. Small schemas never approach the\n * cap, so their output is byte-identical to the fully-inlined form; large\n * aggregates do split (still semantically identical — only the boolean's shape\n * changes).\n */\nexport function generateFast(ir: SchemaIR, g: FastGen): string | null {\n const gen = fastRegistry[ir.type];\n if (gen === null) return null;\n\n if (g.extractable && HOISTABLE.has(ir.type)) {\n const cache = fastSizeCache(g.ctx);\n const overBudget = g.scope.used + predictedInlineSize(ir, g.input.length, cache) > EXTRACT_CAP;\n // Extract a sub-schema big enough to be worth a call; but once the function\n // is ALREADY over budget, extract any hoistable child regardless of size —\n // otherwise a crowd of small siblings (e.g. a discriminated union of many\n // small options) grows the function unbounded since none alone clears\n // MIN_EXTRACT.\n if (overBudget && (g.scope.used > EXTRACT_CAP || estimateFastCost(ir, cache) >= MIN_EXTRACT)) {\n const param = emitTemp(g.ctx, \"op\");\n // Generate the body relative to the helper's parameter, in a fresh scope;\n // the top node is non-extractable (it IS this helper) while its children\n // stay extractable, so an oversized helper splits further recursively.\n // Carry discSkipKey so an extracted discriminated-union option still drops\n // its redundant guard inside the hoisted helper.\n const innerGen = createFastGen(param, g.ctx, false, { temps: [], used: 0 }, g.discSkipKey);\n const inner = generateFast(ir, innerGen);\n if (inner === null) return null; // ineligible sub-schema disables the whole fast path\n const fnName = emitTemp(g.ctx, \"fo\");\n g.ctx.preamble.push(\n `function ${fnName}(${param}){${declareFastTemps(innerGen.scope)}return ${inner};}`,\n );\n g.scope.used += CALL_COST;\n return `${fnName}(${g.input})`;\n }\n }\n\n // Inlined: charge the enclosing function this node's EXACT emitted size. The\n // node's own text plus its inlined descendants is exactly the returned string;\n // children mutated the shared scope as they were visited, but the parent's\n // length is authoritative, so overwrite rather than add.\n const before = g.scope.used;\n // oxlint-disable-next-line typescript/no-explicit-any -- registry dispatch requires type erasure at call site\n const out = (gen as any)(ir, g) as string | null;\n if (out === null) return null;\n g.scope.used = before + out.length;\n return out;\n}\n\n/** Per-compile memo for estimateFastCost, stashed on the shared context. */\nfunction fastSizeCache(ctx: CodeGenContext): WeakMap<SchemaIR, number> {\n return (ctx.fastSizeCache ??= new WeakMap<SchemaIR, number>());\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,MAAM,eAAe;CAEnB,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,MAAM;CACN,WAAW;CACX,MAAM;CACN,KAAK;CACL,OAAO;CACP,KAAK;CACL,SAAS;CACT,SAAS;CACT,MAAM;CAEN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,QAAQ;CACR,KAAK;CACL,KAAK;CACL,MAAM;CAEN,OAAO;CACP,oBAAoB;CACpB,cAAc;CAEd,UAAU;CACV,UAAU;CACV,UAAU;CACV,SAAS;CACT,MAAM;CAEN,QAAQ;CAER,QAAQ;CACR,aAAa;CACb,iBAAiB;CACjB,OAAO;CACP,UAAU;CACV,cAAc;CACd,iBAAiB;CACjB,YAAY;AACd;;;;;;;;;;;AAgBA,SAAgB,cACd,WACA,KACA,cAAc,OACd,QAAmB;CAAE,OAAO,CAAC;CAAG,MAAM;AAAE,GACxC,aACS;CACT,OAAO;EACL,OAAO;EACP;EACA;EACA;EACA;EACA,MAAM,IAAI,WAAW;GAKnB,OAAO,aACL,IACA,cAAc,WAAW,SAAS,WAAW,KAAK,MAAM,OAAO,WAAW,WAAW,CACvF;EACF;EACA,SAAS,UAAU,cAAc,OAAO,KAAK,MAAM;GAAE,OAAO,CAAC;GAAG,MAAM;EAAE,CAAC;EACzE,OAAO,WAAW,SAAS,KAAK,MAAM;EACtC,MAAM,QAAQ;GACZ,MAAM,OAAO,SAAS,KAAK,MAAM;GACjC,MAAM,MAAM,KAAK,IAAI;GACrB,OAAO;EACT;EACA,QAAQ,QAAQ,SAAS,UAAU,UAAU,KAAK,QAAQ,SAAS,KAAK;CAC1E;AACF;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAa,IAAc,GAA2B;CACpE,MAAM,MAAM,aAAa,GAAG;CAC5B,IAAI,QAAQ,MAAM,OAAO;CAEzB,IAAI,EAAE,eAAe,UAAU,IAAI,GAAG,IAAI,GAAG;EAC3C,MAAM,QAAQ,cAAc,EAAE,GAAG;EAOjC,IANmB,EAAE,MAAM,OAAO,oBAAoB,IAAI,EAAE,MAAM,QAAQ,KAAK,IAAA,SAM5D,EAAE,MAAM,OAAA,QAAsB,iBAAiB,IAAI,KAAK,KAAA,OAAmB;GAC5F,MAAM,QAAQ,SAAS,EAAE,KAAK,IAAI;GAMlC,MAAM,WAAW,cAAc,OAAO,EAAE,KAAK,OAAO;IAAE,OAAO,CAAC;IAAG,MAAM;GAAE,GAAG,EAAE,WAAW;GACzF,MAAM,QAAQ,aAAa,IAAI,QAAQ;GACvC,IAAI,UAAU,MAAM,OAAO;GAC3B,MAAM,SAAS,SAAS,EAAE,KAAK,IAAI;GACnC,EAAE,IAAI,SAAS,KACb,YAAY,OAAO,GAAG,MAAM,IAAI,iBAAiB,SAAS,KAAK,EAAE,SAAS,MAAM,GAClF;GACA,EAAE,MAAM,QAAA;GACR,OAAO,GAAG,OAAO,GAAG,EAAE,MAAM;EAC9B;CACF;CAMA,MAAM,SAAS,EAAE,MAAM;CAEvB,MAAM,MAAO,IAAY,IAAI,CAAC;CAC9B,IAAI,QAAQ,MAAM,OAAO;CACzB,EAAE,MAAM,OAAO,SAAS,IAAI;CAC5B,OAAO;AACT;;AAGA,SAAS,cAAc,KAAgD;CACrE,OAAQ,IAAI,kCAAkB,IAAI,QAA0B;AAC9D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fast-size.d.ts","names":[],"sources":["../../../src/core/codegen/fast-size.ts"],"mappings":";;;;;;;;;cA2Ba;;;;;;;iBAiBG,oBACd,IAAI,UACJ,kBACA,OAAO,QAAQ;;;;;;cAUJ;;cAGA;;;;;;;cAQA,WAAW,YAAY;;;;;;;
|
|
1
|
+
{"version":3,"file":"fast-size.d.ts","names":[],"sources":["../../../src/core/codegen/fast-size.ts"],"mappings":";;;;;;;;;cA2Ba;;;;;;;iBAiBG,oBACd,IAAI,UACJ,kBACA,OAAO,QAAQ;;;;;;cAUJ;;cAGA;;;;;;;cAQA,WAAW,YAAY;;;;;;;iBA8FpB,iBAAiB,IAAI,UAAU,OAAO,QAAQ;;;;;;iBA0F9C,mBAAmB,GACjC,gBAAgB,KAChB,OAAO,MAAM,MAAM,UACnB,KAAK,iBACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fast-size.js","names":[],"sources":["../../../src/core/codegen/fast-size.ts"],"sourcesContent":["/**\n * Fast-path size estimation, used to bound the size of each emitted fast-check\n * function. V8's TurboFan refuses to optimize functions whose bytecode exceeds\n * ~60KB and falls back to the weaker Maglev tier — measured ~1.3-1.8x slower on\n * deeply-nested schemas whose monolithic fast-check crosses that budget. By\n * extracting large, self-contained sub-schemas into their own boolean helpers\n * (`generateFast`), each function stays small enough to TurboFan.\n *\n * `estimateFastCost` is a path-agnostic subtree estimate in approximate\n * generated-character units (it can't see the access-path prefix the node will\n * be emitted under, since that lives in the call site, not the IR). The actual\n * emitted size of an inlined node grows with that prefix length — `input[\"a\"]\n * [\"b\"][\"c\"]…` repeated per check — so the extraction decision scales the\n * estimate by `1 + inputLen/PATH_GROWTH_DIVISOR` (see `predictedInlineSize`).\n * The accumulator itself (generateFast) tracks EXACT emitted chars via the\n * returned string length, so only the look-ahead prediction is approximate.\n */\n\nimport type { SchemaIR } from \"../types.js\";\nimport type { CodeGenContext } from \"./context.js\";\n\n/**\n * Per-function soft size cap, in emitted characters. Once the fast-check being\n * assembled would exceed this, the next large hoistable sub-schema is split into\n * its own helper. Set below the ~60KB bytecode point where TurboFan bails (chars\n * over-approximate bytecode) with margin for one look-ahead mis-prediction.\n */\nexport const EXTRACT_CAP = 38_000;\n\n/**\n * Each nesting level lengthens every access path inside a node (`x[\"a\"]` →\n * `x[\"a\"][\"b\"]`), inflating real emitted chars above the path-agnostic estimate.\n * The extraction look-ahead multiplies the estimate by `1 + inputLen/this` so a\n * deep subtree is split before it inlines into an over-budget function. Smaller\n * ⇒ more eager splitting at depth. Calibrated against deeply-nested fixtures.\n */\nconst PATH_GROWTH_DIVISOR = 26;\n\n/**\n * Predicted emitted size of inlining `ir` at a call site whose input expression\n * is `inputLen` chars long — the path-agnostic subtree estimate scaled up for\n * the access-path prefix that estimate omits. Used only for the extract/inline\n * decision; the running total uses exact emitted lengths.\n */\nexport function predictedInlineSize(\n ir: SchemaIR,\n inputLen: number,\n cache: WeakMap<SchemaIR, number>,\n): number {\n return estimateFastCost(ir, cache) * (1 + inputLen / PATH_GROWTH_DIVISOR);\n}\n\n/**\n * Floor for extraction: never hoist a sub-schema smaller than this even when\n * the enclosing function is over the cap — a tiny helper trades a real call for\n * no optimization benefit. Only sub-schemas big enough to matter are split.\n */\nexport const MIN_EXTRACT = 1_200;\n\n/** Approximate size contributed to the enclosing function by an extracted call `__fo_N(expr)`. */\nexport const CALL_COST = 24;\n\n/**\n * Node types whose fast-check is a self-contained boolean over a single input\n * expression and large enough to be worth hoisting into `function f(p){return …}`.\n * Thin wrappers (optional/nullable/default/…) are omitted: their inner schema is\n * visited through `generateFast` and gets hoisted on its own when large.\n */\nexport const HOISTABLE: ReadonlySet<SchemaIR[\"type\"]> = new Set([\n \"object\",\n \"record\",\n \"tuple\",\n \"discriminatedUnion\",\n \"union\",\n \"intersection\",\n]);\n\n/** A node's own contribution to the emitted fast-check, EXCLUDING its children. */\nfunction shallowFastCost(ir: SchemaIR): number {\n switch (ir.type) {\n case \"object\":\n return 50 + Object.keys(ir.properties).length * 25 + (ir.strict ? 120 : 0);\n case \"string\":\n return 20 + ir.checks.length * 35;\n case \"number\":\n return 25 + ir.checks.length * 30;\n case \"bigint\":\n case \"date\":\n return 25 + (ir.checks?.length ?? 0) * 25;\n case \"enum\":\n return 15 + ir.values.length * 12;\n case \"tuple\":\n return 40 + ir.items.length * 15;\n case \"union\":\n return 12 + ir.options.length * 6;\n case \"discriminatedUnion\":\n return 45 + ir.cases.length * 12;\n case \"array\":\n case \"record\":\n case \"set\":\n case \"map\":\n return 55;\n case \"file\":\n case \"templateLiteral\":\n return 40;\n case \"custom\":\n return 30;\n case \"literal\":\n case \"optional\":\n case \"nullable\":\n case \"default\":\n return 22;\n case \"recursiveRef\":\n case \"recursionTarget\":\n // Both emit just a call to a hosted helper; the target's `inner` is sized\n // independently when its own helper body is generated, so it is NOT\n // counted against the enclosing function here (see fastChildren).\n return 15;\n default:\n return 18;\n }\n}\n\n/** Child IR nodes that the fast generator recurses into (mirrors the fast schema generators). */\nfunction fastChildren(ir: SchemaIR): readonly SchemaIR[] {\n switch (ir.type) {\n case \"object\":\n return Object.values(ir.properties);\n case \"array\":\n return [ir.element];\n case \"tuple\":\n return ir.rest !== null ? [...ir.items, ir.rest] : ir.items;\n case \"record\":\n case \"map\":\n return [ir.keyType, ir.valueType];\n case \"set\":\n return [ir.valueType];\n case \"union\":\n case \"discriminatedUnion\":\n return ir.options;\n case \"intersection\":\n return [ir.left, ir.right];\n case \"pipe\":\n return [ir.in, ir.out];\n case \"optional\":\n case \"nullable\":\n case \"readonly\":\n case \"default\":\n case \"catch\":\n return [ir.inner];\n default:\n return [];\n }\n}\n\n/**\n * Estimated total size of a node's fast-check (the node plus everything inlined\n * beneath it). Memoized per IR node: schemas dedupe shared sub-trees and the\n * estimate is consulted at every nesting level, so without the cache a deep tree\n * would be re-walked quadratically.\n */\nexport function estimateFastCost(ir: SchemaIR, cache: WeakMap<SchemaIR, number>): number {\n const cached = cache.get(ir);\n if (cached !== undefined) return cached;\n // Defensive cycle break (the IR is a tree today; recursiveRef is a leaf).\n cache.set(ir, 0);\n let total = shallowFastCost(ir);\n for (const child of fastChildren(ir)) total += estimateFastCost(child, cache);\n cache.set(ir, total);\n return total;\n}\n\n// ─── Runtime cost (check ordering) ───────────────────────────────────────────\n\n/**\n * Assumed element count for unbounded collections, used only to weight a\n * container against its siblings when ordering an `&&` chain.\n */\nconst ASSUMED_ELEMENTS = 4;\n\n/** Runtime weight of one check on an already-type-confirmed value. */\nfunction checkRuntimeCost(kind: string): number {\n switch (kind) {\n // A `.test()` call costs ~9 ns of dispatch before it matches a character —\n // an order of magnitude above every other check, and the reason ordering\n // matters at all.\n case \"string_format\":\n return 100;\n // Zero-capture user predicate: a real call, cheaper than a regex.\n case \"refine_effect\":\n return 20;\n case \"includes\":\n case \"starts_with\":\n case \"ends_with\":\n return 6;\n case \"multiple_of\":\n case \"bigint_multiple_of\":\n return 4;\n default:\n // length/size/range comparisons: a load and a compare.\n return 1;\n }\n}\n\n/** A node's own runtime weight, EXCLUDING its children. */\nfunction shallowRuntimeCost(ir: SchemaIR): number {\n switch (ir.type) {\n case \"string\":\n case \"number\":\n return 2 + ir.checks.reduce((sum, c) => sum + checkRuntimeCost(c.kind), 0);\n case \"bigint\":\n case \"date\":\n return 3 + (ir.checks?.length ?? 0);\n case \"file\":\n return 3 + (ir.checks?.length ?? 0) * 2;\n case \"enum\":\n // Inlined `===` chain (or one hashed lookup) — sub-nanosecond either way.\n return 1 + Math.min(ir.values.length, 8);\n case \"object\":\n // Type guard, plus the strict pass's per-key membership test.\n return 3 + (ir.strict === true ? Object.keys(ir.properties).length : 0);\n case \"array\":\n case \"set\":\n case \"record\":\n case \"map\":\n case \"tuple\":\n return 3;\n case \"templateLiteral\":\n return 100;\n case \"custom\":\n return 20;\n case \"recursiveRef\":\n case \"recursionTarget\":\n // A recursive call expands into an unknown amount of work; treat it as\n // the most expensive sibling so it is probed last.\n return 200;\n default:\n return 1;\n }\n}\n\n/** estimateRuntimeCost against the per-compile memo hanging off the context. */\nfunction runtimeCostOf(ir: SchemaIR, ctx: CodeGenContext): number {\n return estimateRuntimeCost(ir, (ctx.fastRuntimeCostCache ??= new WeakMap<SchemaIR, number>()));\n}\n\n/**\n * Sort a fast-check's sibling conjuncts (or union options) cheapest-first.\n * Returns a new array; ties keep source order (Array#sort is stable), so a\n * schema whose members all cost the same emits byte-identical output.\n */\nexport function orderByRuntimeCost<T>(\n items: readonly T[],\n irOf: (item: T) => SchemaIR,\n ctx: CodeGenContext,\n): T[] {\n if (items.length < 2) return [...items];\n return [...items].sort((a, b) => runtimeCostOf(irOf(a), ctx) - runtimeCostOf(irOf(b), ctx));\n}\n\n/**\n * Estimated runtime cost of a node's fast-check, in arbitrary units calibrated\n * so a regex format check dominates a handful of type guards. Memoized per IR\n * node, like estimateFastCost — the estimate is consulted at every nesting\n * level, and shared sub-trees are common.\n *\n * Used to order the conjuncts of an `&&` chain cheapest-first. Accepting input\n * runs every conjunct regardless, so ordering is free there; REJECTING input\n * stops at the first false one, which is where a schema pays for probing a\n * `z.email()` before the `kind` literal that actually discriminates. Measured\n * on a 3-option union of objects that each declare `{email, kind, note}`:\n * 102 ns → 39 ns for an input matching the last option, and 102 ns → 6 ns for\n * one matching none.\n */\nfunction estimateRuntimeCost(ir: SchemaIR, cache: WeakMap<SchemaIR, number>): number {\n const cached = cache.get(ir);\n if (cached !== undefined) return cached;\n cache.set(ir, 0); // cycle break (mirrors estimateFastCost)\n let total = shallowRuntimeCost(ir);\n const children = fastChildren(ir);\n switch (ir.type) {\n case \"array\":\n case \"set\":\n case \"record\":\n case \"map\":\n // Per-element work, repeated for an assumed handful of entries.\n for (const child of children) total += estimateRuntimeCost(child, cache) * ASSUMED_ELEMENTS;\n break;\n case \"discriminatedUnion\":\n // O(1) switch: only the matched case runs. Charge the priciest option so\n // the estimate stays an upper bound.\n total += children.reduce((max, c) => Math.max(max, estimateRuntimeCost(c, cache)), 0);\n break;\n default:\n // object/tuple/union/intersection/wrappers: every child is on the path\n // (a union probes options until one matches — worst case, all of them).\n for (const child of children) total += estimateRuntimeCost(child, cache);\n break;\n }\n cache.set(ir, total);\n return total;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fast-size.js","names":[],"sources":["../../../src/core/codegen/fast-size.ts"],"sourcesContent":["/**\n * Fast-path size estimation, used to bound the size of each emitted fast-check\n * function. V8's TurboFan refuses to optimize functions whose bytecode exceeds\n * ~60KB and falls back to the weaker Maglev tier — measured ~1.3-1.8x slower on\n * deeply-nested schemas whose monolithic fast-check crosses that budget. By\n * extracting large, self-contained sub-schemas into their own boolean helpers\n * (`generateFast`), each function stays small enough to TurboFan.\n *\n * `estimateFastCost` is a path-agnostic subtree estimate in approximate\n * generated-character units (it can't see the access-path prefix the node will\n * be emitted under, since that lives in the call site, not the IR). The actual\n * emitted size of an inlined node grows with that prefix length — `input[\"a\"]\n * [\"b\"][\"c\"]…` repeated per check — so the extraction decision scales the\n * estimate by `1 + inputLen/PATH_GROWTH_DIVISOR` (see `predictedInlineSize`).\n * The accumulator itself (generateFast) tracks EXACT emitted chars via the\n * returned string length, so only the look-ahead prediction is approximate.\n */\n\nimport type { SchemaIR } from \"../types.js\";\nimport type { CodeGenContext } from \"./context.js\";\n\n/**\n * Per-function soft size cap, in emitted characters. Once the fast-check being\n * assembled would exceed this, the next large hoistable sub-schema is split into\n * its own helper. Set below the ~60KB bytecode point where TurboFan bails (chars\n * over-approximate bytecode) with margin for one look-ahead mis-prediction.\n */\nexport const EXTRACT_CAP = 38_000;\n\n/**\n * Each nesting level lengthens every access path inside a node (`x[\"a\"]` →\n * `x[\"a\"][\"b\"]`), inflating real emitted chars above the path-agnostic estimate.\n * The extraction look-ahead multiplies the estimate by `1 + inputLen/this` so a\n * deep subtree is split before it inlines into an over-budget function. Smaller\n * ⇒ more eager splitting at depth. Calibrated against deeply-nested fixtures.\n */\nconst PATH_GROWTH_DIVISOR = 26;\n\n/**\n * Predicted emitted size of inlining `ir` at a call site whose input expression\n * is `inputLen` chars long — the path-agnostic subtree estimate scaled up for\n * the access-path prefix that estimate omits. Used only for the extract/inline\n * decision; the running total uses exact emitted lengths.\n */\nexport function predictedInlineSize(\n ir: SchemaIR,\n inputLen: number,\n cache: WeakMap<SchemaIR, number>,\n): number {\n return estimateFastCost(ir, cache) * (1 + inputLen / PATH_GROWTH_DIVISOR);\n}\n\n/**\n * Floor for extraction: never hoist a sub-schema smaller than this even when\n * the enclosing function is over the cap — a tiny helper trades a real call for\n * no optimization benefit. Only sub-schemas big enough to matter are split.\n */\nexport const MIN_EXTRACT = 1_200;\n\n/** Approximate size contributed to the enclosing function by an extracted call `__fo_N(expr)`. */\nexport const CALL_COST = 24;\n\n/**\n * Node types whose fast-check is a self-contained boolean over a single input\n * expression and large enough to be worth hoisting into `function f(p){return …}`.\n * Thin wrappers (optional/nullable/default/…) are omitted: their inner schema is\n * visited through `generateFast` and gets hoisted on its own when large.\n */\nexport const HOISTABLE: ReadonlySet<SchemaIR[\"type\"]> = new Set([\n \"object\",\n \"record\",\n \"tuple\",\n \"discriminatedUnion\",\n \"union\",\n \"intersection\",\n]);\n\n/** A node's own contribution to the emitted fast-check, EXCLUDING its children. */\nfunction shallowFastCost(ir: SchemaIR): number {\n switch (ir.type) {\n case \"object\":\n return 50 + Object.keys(ir.properties).length * 25 + (ir.strict ? 120 : 0);\n case \"string\":\n return 20 + ir.checks.length * 35;\n case \"number\":\n return 25 + ir.checks.length * 30;\n case \"bigint\":\n case \"date\":\n return 25 + (ir.checks?.length ?? 0) * 25;\n case \"enum\":\n return 15 + ir.values.length * 12;\n case \"tuple\":\n return 40 + ir.items.length * 15;\n case \"union\":\n return 12 + ir.options.length * 6;\n case \"discriminatedUnion\":\n return 45 + ir.cases.length * 12;\n case \"array\":\n case \"record\":\n case \"set\":\n case \"map\":\n return 55;\n case \"file\":\n case \"templateLiteral\":\n return 40;\n case \"custom\":\n return 30;\n case \"literal\":\n case \"optional\":\n case \"nullable\":\n case \"default\":\n return 22;\n case \"recursiveRef\":\n case \"recursionTarget\":\n // Both emit just a call to a hosted helper; the target's `inner` is sized\n // independently when its own helper body is generated, so it is NOT\n // counted against the enclosing function here (see fastChildren).\n return 15;\n default:\n return 18;\n }\n}\n\n/** Child IR nodes that the fast generator recurses into (mirrors the fast schema generators). */\nfunction fastChildren(ir: SchemaIR): readonly SchemaIR[] {\n switch (ir.type) {\n case \"object\":\n return Object.values(ir.properties);\n case \"array\":\n return [ir.element];\n case \"tuple\":\n return ir.rest !== null ? [...ir.items, ir.rest] : ir.items;\n case \"record\":\n case \"map\":\n return [ir.keyType, ir.valueType];\n case \"set\":\n return [ir.valueType];\n case \"union\":\n case \"discriminatedUnion\":\n return ir.options;\n case \"intersection\":\n return [ir.left, ir.right];\n case \"pipe\":\n return [ir.in, ir.out];\n case \"optional\":\n case \"nullable\":\n case \"readonly\":\n case \"default\":\n case \"catch\":\n case \"zodDelegate\":\n return [ir.inner];\n default:\n return [];\n }\n}\n\n/**\n * Estimated total size of a node's fast-check (the node plus everything inlined\n * beneath it). Memoized per IR node: schemas dedupe shared sub-trees and the\n * estimate is consulted at every nesting level, so without the cache a deep tree\n * would be re-walked quadratically.\n */\nexport function estimateFastCost(ir: SchemaIR, cache: WeakMap<SchemaIR, number>): number {\n const cached = cache.get(ir);\n if (cached !== undefined) return cached;\n // Defensive cycle break (the IR is a tree today; recursiveRef is a leaf).\n cache.set(ir, 0);\n let total = shallowFastCost(ir);\n for (const child of fastChildren(ir)) total += estimateFastCost(child, cache);\n cache.set(ir, total);\n return total;\n}\n\n// ─── Runtime cost (check ordering) ───────────────────────────────────────────\n\n/**\n * Assumed element count for unbounded collections, used only to weight a\n * container against its siblings when ordering an `&&` chain.\n */\nconst ASSUMED_ELEMENTS = 4;\n\n/** Runtime weight of one check on an already-type-confirmed value. */\nfunction checkRuntimeCost(kind: string): number {\n switch (kind) {\n // A `.test()` call costs ~9 ns of dispatch before it matches a character —\n // an order of magnitude above every other check, and the reason ordering\n // matters at all.\n case \"string_format\":\n return 100;\n // Zero-capture user predicate: a real call, cheaper than a regex.\n case \"refine_effect\":\n return 20;\n case \"includes\":\n case \"starts_with\":\n case \"ends_with\":\n return 6;\n case \"multiple_of\":\n case \"bigint_multiple_of\":\n return 4;\n default:\n // length/size/range comparisons: a load and a compare.\n return 1;\n }\n}\n\n/** A node's own runtime weight, EXCLUDING its children. */\nfunction shallowRuntimeCost(ir: SchemaIR): number {\n switch (ir.type) {\n case \"string\":\n case \"number\":\n return 2 + ir.checks.reduce((sum, c) => sum + checkRuntimeCost(c.kind), 0);\n case \"bigint\":\n case \"date\":\n return 3 + (ir.checks?.length ?? 0);\n case \"file\":\n return 3 + (ir.checks?.length ?? 0) * 2;\n case \"enum\":\n // Inlined `===` chain (or one hashed lookup) — sub-nanosecond either way.\n return 1 + Math.min(ir.values.length, 8);\n case \"object\":\n // Type guard, plus the strict pass's per-key membership test.\n return 3 + (ir.strict === true ? Object.keys(ir.properties).length : 0);\n case \"array\":\n case \"set\":\n case \"record\":\n case \"map\":\n case \"tuple\":\n return 3;\n case \"templateLiteral\":\n return 100;\n case \"custom\":\n return 20;\n case \"recursiveRef\":\n case \"recursionTarget\":\n // A recursive call expands into an unknown amount of work; treat it as\n // the most expensive sibling so it is probed last.\n return 200;\n default:\n return 1;\n }\n}\n\n/** estimateRuntimeCost against the per-compile memo hanging off the context. */\nfunction runtimeCostOf(ir: SchemaIR, ctx: CodeGenContext): number {\n return estimateRuntimeCost(ir, (ctx.fastRuntimeCostCache ??= new WeakMap<SchemaIR, number>()));\n}\n\n/**\n * Sort a fast-check's sibling conjuncts (or union options) cheapest-first.\n * Returns a new array; ties keep source order (Array#sort is stable), so a\n * schema whose members all cost the same emits byte-identical output.\n */\nexport function orderByRuntimeCost<T>(\n items: readonly T[],\n irOf: (item: T) => SchemaIR,\n ctx: CodeGenContext,\n): T[] {\n if (items.length < 2) return [...items];\n return [...items].sort((a, b) => runtimeCostOf(irOf(a), ctx) - runtimeCostOf(irOf(b), ctx));\n}\n\n/**\n * Estimated runtime cost of a node's fast-check, in arbitrary units calibrated\n * so a regex format check dominates a handful of type guards. Memoized per IR\n * node, like estimateFastCost — the estimate is consulted at every nesting\n * level, and shared sub-trees are common.\n *\n * Used to order the conjuncts of an `&&` chain cheapest-first. Accepting input\n * runs every conjunct regardless, so ordering is free there; REJECTING input\n * stops at the first false one, which is where a schema pays for probing a\n * `z.email()` before the `kind` literal that actually discriminates. Measured\n * on a 3-option union of objects that each declare `{email, kind, note}`:\n * 102 ns → 39 ns for an input matching the last option, and 102 ns → 6 ns for\n * one matching none.\n */\nfunction estimateRuntimeCost(ir: SchemaIR, cache: WeakMap<SchemaIR, number>): number {\n const cached = cache.get(ir);\n if (cached !== undefined) return cached;\n cache.set(ir, 0); // cycle break (mirrors estimateFastCost)\n let total = shallowRuntimeCost(ir);\n const children = fastChildren(ir);\n switch (ir.type) {\n case \"array\":\n case \"set\":\n case \"record\":\n case \"map\":\n // Per-element work, repeated for an assumed handful of entries.\n for (const child of children) total += estimateRuntimeCost(child, cache) * ASSUMED_ELEMENTS;\n break;\n case \"discriminatedUnion\":\n // O(1) switch: only the matched case runs. Charge the priciest option so\n // the estimate stays an upper bound.\n total += children.reduce((max, c) => Math.max(max, estimateRuntimeCost(c, cache)), 0);\n break;\n default:\n // object/tuple/union/intersection/wrappers: every child is on the path\n // (a union probes options until one matches — worst case, all of them).\n for (const child of children) total += estimateRuntimeCost(child, cache);\n break;\n }\n cache.set(ir, total);\n return total;\n}\n"],"mappings":"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"}
|
|
@@ -81,6 +81,22 @@ function generateValidator(ir, name, options) {
|
|
|
81
81
|
fastExpr = `${fastFnName}(input)`;
|
|
82
82
|
}
|
|
83
83
|
const buildFnName = generateBuild(ir, ctx);
|
|
84
|
+
if (ir.type === "zodDelegate" && rebuildsOutput(ir.inner) && buildFnName === null) {
|
|
85
|
+
ctx.preamble.length = 0;
|
|
86
|
+
ctx.usedHelpers.clear();
|
|
87
|
+
ctx.regexCache.clear();
|
|
88
|
+
ctx.effectFnCache?.clear();
|
|
89
|
+
ctx.valueCache?.clear();
|
|
90
|
+
const delegate = emitRfDelegate(ctx, ir.refIndex);
|
|
91
|
+
return {
|
|
92
|
+
code: ["/* zod-compiler */", ...ctx.preamble].join("\n"),
|
|
93
|
+
functionDef: `function ${fnName}(input){return ${delegate}(input);}`,
|
|
94
|
+
refCount: options?.refCount ?? 0,
|
|
95
|
+
usedHelpers: ctx.usedHelpers,
|
|
96
|
+
fastFnName: null,
|
|
97
|
+
fastTotal: false
|
|
98
|
+
};
|
|
99
|
+
}
|
|
84
100
|
const buildIsFnName = ctx.buildSubstitutesValue === true ? null : fastFnName;
|
|
85
101
|
const baseRefCount = options?.refCount ?? 0;
|
|
86
102
|
if (options?.compact === true && fastExpr !== null && fastExpr !== "true" && !hasMutation(ir) && !hasNonRootTargets) {
|
|
@@ -239,7 +255,8 @@ function childIRs(ir) {
|
|
|
239
255
|
case "default":
|
|
240
256
|
case "catch":
|
|
241
257
|
case "effect":
|
|
242
|
-
case "recursionTarget":
|
|
258
|
+
case "recursionTarget":
|
|
259
|
+
case "zodDelegate": return [ir.inner];
|
|
243
260
|
case "pipe": return [ir.in, ir.out];
|
|
244
261
|
default: return [];
|
|
245
262
|
}
|