From f210b1102abf9c57e229d7610567cca9dd45c287 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 16:38:34 -0700 Subject: [PATCH 01/30] Run zod 4's core constructor factory in the dynamic tier zod 4 builds every schema through `$constructor`, a factory of JavaScript patterns the dynamic tier refused or answered wrongly. Each is fixed generally: - `"key" in BuiltinCtor` (`"captureStackTrace" in Error`) folds to Node's answer from a table of the builtin constructor globals' keys. - A class whose heritage evaluates to Object or Error at runtime (`class D extends (params?.Parent ?? Object)`): Object means no heritage; Error links Error.prototype and super() installs message, cause and stack. - `Object.defineProperty` on a class value boxes to its constructor, and a JavaScript class targeted by defineProperty gets a live static table. - A Symbol.hasInstance defined on a function's own table answers untyped `instanceof`; an error constructor held as a value answers by error kind. - A builtin error constructor stored in a slot of its class value type (`{ Parent: Error }`) is the class value, not a checked cast that fails. - A function hoisted above a local class it references captures the class binding as a TDZ box the class statement initializes. - Names a program stores on the global object read back from it instead of folding to undefined. - A module namespace whose exports never change converts to an untyped object (`{ ...ns }`). - An untyped call of a String-only method name (`x.trim()`) dispatches on the receiver's runtime kind, so objects defining that member work. Co-Authored-By: Claude Opus 5.5 --- .../builtins/global-namespace-keys.ts | 123 +++++++++++++++++ .../lowering/expressions/identifiers.ts | 7 + .../lowering/expressions/property-access.ts | 41 ++++++ .../lowering/expressions/relational.ts | 11 ++ .../src/frontend/lowering/lower-calls.ts | 13 +- .../src/frontend/lowering/lower-classes.ts | 43 +++++- .../src/frontend/lowering/lower-stmts.ts | 46 +++++++ .../compiler/src/frontend/lowering/lowerer.ts | 24 ++++ .../lowering/module-namespace-elements.ts | 82 ++++++++++- .../7480-builtin-constructor-in.js.json | 6 + ...lass-runtime-heritage-object-error.js.json | 6 + ...ine-property-function-class-values.js.json | 6 + .../7483-constructor-factory-traits.js.json | 6 + ...sted-function-captures-later-class.js.json | 6 + .../7485-global-object-own-properties.js.json | 6 + ...otype-accessor-string-method-names.js.json | 6 + .../main.js.json | 8 ++ packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_dyn_builtin_class.c | 2 + packages/runtime/src/scr_json.c | 128 ++++++++++++++++++ packages/runtime/src/scr_symbol.c | 5 + tests/corpus/7480-builtin-constructor-in.js | 9 ++ ...481-class-runtime-heritage-object-error.js | 24 ++++ ...2-define-property-function-class-values.js | 35 +++++ .../corpus/7483-constructor-factory-traits.js | 58 ++++++++ ...4-hoisted-function-captures-later-class.js | 23 ++++ .../7485-global-object-own-properties.js | 22 +++ ...-prototype-accessor-string-method-names.js | 53 ++++++++ .../7487-namespace-spread-untyped/extra.js | 2 + .../7487-namespace-spread-untyped/main.js | 11 ++ .../7487-namespace-spread-untyped/shapes.js | 4 + .../7480-builtin-constructor-in.js.json | 1 + ...lass-runtime-heritage-object-error.js.json | 1 + ...ine-property-function-class-values.js.json | 1 + .../7483-constructor-factory-traits.js.json | 1 + ...sted-function-captures-later-class.js.json | 1 + .../7485-global-object-own-properties.js.json | 1 + ...otype-accessor-string-method-names.js.json | 1 + .../main.js.json | 1 + 39 files changed, 821 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json create mode 100644 tests/corpus/7480-builtin-constructor-in.js create mode 100644 tests/corpus/7481-class-runtime-heritage-object-error.js create mode 100644 tests/corpus/7482-define-property-function-class-values.js create mode 100644 tests/corpus/7483-constructor-factory-traits.js create mode 100644 tests/corpus/7484-hoisted-function-captures-later-class.js create mode 100644 tests/corpus/7485-global-object-own-properties.js create mode 100644 tests/corpus/7486-prototype-accessor-string-method-names.js create mode 100644 tests/corpus/7487-namespace-spread-untyped/extra.js create mode 100644 tests/corpus/7487-namespace-spread-untyped/main.js create mode 100644 tests/corpus/7487-namespace-spread-untyped/shapes.js create mode 100644 tests/harness/corpus-costs/7480-builtin-constructor-in.js.json create mode 100644 tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json create mode 100644 tests/harness/corpus-costs/7482-define-property-function-class-values.js.json create mode 100644 tests/harness/corpus-costs/7483-constructor-factory-traits.js.json create mode 100644 tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json create mode 100644 tests/harness/corpus-costs/7485-global-object-own-properties.js.json create mode 100644 tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json create mode 100644 tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json diff --git a/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts new file mode 100644 index 000000000..bb364ee55 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts @@ -0,0 +1,123 @@ +/* The string-keyed properties Node 24 exposes on its builtin constructor + * and namespace globals, own and inherited, so `"key" in Error` (a feature + * probe such as `"captureStackTrace" in Error`) answers statically. The + * global must be the unshadowed builtin; compiled programs cannot add + * properties to these objects, so the answer is fixed. */ + +const FUNCTION_PROTOTYPE = [ + "length", + "name", + "arguments", + "caller", + "constructor", + "apply", + "bind", + "call", + "toString", +]; +const OBJECT_PROTOTYPE = [ + "constructor", + "__defineGetter__", + "__defineSetter__", + "hasOwnProperty", + "__lookupGetter__", + "__lookupSetter__", + "isPrototypeOf", + "propertyIsEnumerable", + "toString", + "valueOf", + "__proto__", + "toLocaleString", +]; +const CONSTRUCTOR_OWN = ["length", "name", "prototype"]; +const ERROR_OWN = [ + ...CONSTRUCTOR_OWN, + "captureStackTrace", + "prepareStackTrace", + "isError", + "stackTraceLimit", +]; + +/** Own string keys per global, beyond what the prototype chain adds. */ +const CONSTRUCTORS: Readonly> = { + Error: ERROR_OWN, + // The native error constructors inherit from Error. + TypeError: ERROR_OWN, + RangeError: ERROR_OWN, + SyntaxError: ERROR_OWN, + ReferenceError: ERROR_OWN, + EvalError: ERROR_OWN, + URIError: ERROR_OWN, + Object: [ + ...CONSTRUCTOR_OWN, + "assign", + "getOwnPropertyDescriptor", + "getOwnPropertyDescriptors", + "getOwnPropertyNames", + "getOwnPropertySymbols", + "hasOwn", + "is", + "preventExtensions", + "seal", + "create", + "defineProperties", + "defineProperty", + "freeze", + "getPrototypeOf", + "setPrototypeOf", + "isExtensible", + "isFrozen", + "isSealed", + "keys", + "entries", + "fromEntries", + "values", + "groupBy", + ], + Array: [...CONSTRUCTOR_OWN, "isArray", "from", "fromAsync", "of"], + Promise: [ + ...CONSTRUCTOR_OWN, + "all", + "allSettled", + "any", + "race", + "resolve", + "reject", + "withResolvers", + "try", + ], + Number: [ + ...CONSTRUCTOR_OWN, + "isFinite", + "isInteger", + "isNaN", + "isSafeInteger", + "parseFloat", + "parseInt", + "MAX_VALUE", + "MIN_VALUE", + "NaN", + "NEGATIVE_INFINITY", + "POSITIVE_INFINITY", + "MAX_SAFE_INTEGER", + "MIN_SAFE_INTEGER", + "EPSILON", + ], + String: [...CONSTRUCTOR_OWN, "fromCharCode", "fromCodePoint", "raw"], + Map: [...CONSTRUCTOR_OWN, "groupBy"], + Set: CONSTRUCTOR_OWN, + WeakMap: CONSTRUCTOR_OWN, + WeakSet: CONSTRUCTOR_OWN, + Boolean: CONSTRUCTOR_OWN, + Function: CONSTRUCTOR_OWN, + Date: [...CONSTRUCTOR_OWN, "now", "parse", "UTC"], + BigInt: [...CONSTRUCTOR_OWN, "asUintN", "asIntN"], +}; + +/** Whether Node answers `key in ` with true, or null when `name` is + * not a tabled global. */ +export function builtinGlobalHasKey(name: string, key: string): boolean | null { + const own = CONSTRUCTORS[name]; + if (own === undefined) return null; + return own.includes(key) || FUNCTION_PROTOTYPE.includes(key) || OBJECT_PROTOTYPE.includes(key); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index eca3c33f5..e0f43cc4a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -1,4 +1,5 @@ /* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */ +import { predeclareForwardClass } from "../lower-stmts.js"; import { cjsMemberAliasValueOf } from "../cjs-exports-object.js"; import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js"; import * as ts from "../../ts7/adapter.js"; @@ -862,6 +863,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: { const sym = lowerer.resolveValueSymbol(expr); const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; + // A local class referenced from a function hoisted above its statement: + // its TDZ box, holding the class as a checked-dynamic constructor. + if (!classInfo && sym && predeclareForwardClass(lowerer, sym)) { + const local = lowerer.resolveLocal(expr); + if (local) return { kind: "varRef", localId: local.id, type: local.type, loc }; + } // A GENERIC class name whose CONTEXTUAL type pins one instantiation // (`const B: new (v: string) => Counter = Counter`): the // value is that instantiation's class object — the generic-fn diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 5df3c7fe0..d1b9795d5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -553,6 +553,19 @@ export function lowerPropertyAccessExpr( expr.name.text, ) ) { + // A name the program stores on the global object + // (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from + // the global object's own properties. + const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression)); + if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) { + return { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }; + } return dynUndefinedExpr(loc); } // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex @@ -4490,3 +4503,31 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { } return count; } + +const globalThisWrites = new WeakMap>(); + +/** The property names the program assigns on the global object, through + * `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */ +function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set { + const known = globalThisWrites.get(lowerer); + if (known) return known; + const names = new Set(); + globalThisWrites.set(lowerer, names); + for (const sf of lowerer.program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue; + ts.walkPreorder(sf, (node) => { + if ( + ts.isBinaryExpression(node) && + (node.operatorToken.kind === ts.SyntaxKind.EqualsToken || + node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken || + node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken || + node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) && + ts.isPropertyAccessExpression(node.left) && + lowerer.typeOf(node.left.expression) === globalType + ) + names.add(node.left.name.text); + return undefined; + }); + } + return names; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 759687ff0..24a582746 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -51,6 +51,7 @@ import { isJsSourceFile, locOf } from "../../program.js"; import { checkedClassInstanceOf } from "../class-construction.js"; import { unsupportedDiag } from "../../../diagnostics/diagnostic.js"; import { globalSymbolKey } from "./global-symbols.js"; +import { builtinGlobalHasKey } from "../builtins/global-namespace-keys.js"; import { foldedStringKeyOf } from "./object-literals.js"; import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js"; import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js"; @@ -1426,6 +1427,16 @@ export function lowerInExpression( "'in' with computed (non-literal) keys (a const whose type is one string literal folds; a string-typed key answers over index-signature record receivers)", ); } + // A feature probe on a builtin constructor global (`"captureStackTrace" + // in Error`): the global is a fixed object, so the answer is static. + { + let recvNode = expr.right; + while (ts.isParenthesizedExpression(recvNode)) recvNode = recvNode.expression; + if (ts.isIdentifier(recvNode) && lowerer.isStdlibGlobal(recvNode, recvNode.text)) { + const has = builtinGlobalHasKey(recvNode.text, key); + if (has !== null) return { kind: "boolLit", value: has, type: BOOL, loc }; + } + } // A receiver re-read (the static folds below) is safe for plain reads // AND for side-effect-free expressions (an object literal of literals // — the `"a" in { a: true }` shape). diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c0406e77b..42e3a20cf 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3804,11 +3804,12 @@ function lowerDynReceiverMethodCall( if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { // An object-typed receiver the member is missing from (a JavaScript // module object whose members are assigned in a function body, - // `common.repeat(" ", n)`) holds a string or an object with that + // `common.repeat(" ", n)`), or an untyped receiver (an object whose + // prototype defines `trim`), holds a string or an object with that // member: dispatch on the receiver's runtime kind. if ( - !checkerUntyped && - (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !call.questionDotToken && !access.questionDotToken && !call.arguments.nodes.some(ts.isSpreadElement) @@ -8233,6 +8234,12 @@ function lowerObjectStaticCall( ...(fnName !== null ? { fnName } : {}), }; } + // A class value (`Object.defineProperty(Definition, "name", { value })`) + // boxes to its constructor function, whose own-property table holds the + // definition for every alias of the class. + if (target?.type.kind === "classval" && lowerer.dynConvertible(target.type)) { + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; + } const descriptorPrimitive = member === "getOwnPropertyDescriptor" && target && diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b23697cab..d283067cd 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -5,6 +5,7 @@ import { isDatePrototypeMethod, lowerDateSuperMethodCall, } from "./builtins/date-classes.js"; +import { predeclareForwardClass } from "./lower-stmts.js"; import { lowerDateStaticCall } from "./builtins/dates.js"; import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; @@ -2355,6 +2356,7 @@ export function collectClassShapeInner( const checkedStatics = isJsSourceFile(ts.getSourceFileOfNode(decl)) && ((base && hasRuntimeStatics(base)) || + definedThroughReflection(lowerer, decl) || decl.members.nodes.some( (member) => ts.isAccessor(member) && @@ -5476,7 +5478,12 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) const display = info.def.name.replace(/^%|^%m\d+\./, ""); fenceDecorationThrows(lowerer, info, blame); if (info.localClass && ts.isIdentifier(blame)) { - const stored = lowerer.resolveLocal(blame); + let stored = lowerer.resolveLocal(blame); + // A reference from a function hoisted above the class statement. + if (!stored) { + const symbol = lowerer.resolveValueSymbol(blame); + if (symbol && predeclareForwardClass(lowerer, symbol)) stored = lowerer.resolveLocal(blame); + } if (stored) return lowerer.coerceToExpected( { kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) }, @@ -11152,3 +11159,37 @@ function genericFieldFnNodeOf( } return null; } + +/** Whether a JavaScript class's own binding is the target of + * `Object.defineProperty`/`defineProperties` in its file + * (`Object.defineProperty(Definition, "name", { value })`): its constructor + * then needs a live own-property table. */ +function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean { + const name = decl.name; + const sf = ts.getSourceFileOfNode(decl); + if (name === undefined || !sf.text.includes("definePropert")) return false; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (symbol === undefined) return false; + let found = false; + ts.walkPreorder(sf, (node) => { + if (found) return "stop"; + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + (node.expression.name.text === "defineProperty" || + node.expression.name.text === "defineProperties") && + lowerer.stdlibGlobalMember(node.expression, "Object") !== null + ) { + const target = node.arguments.nodes[0]; + if ( + target !== undefined && + ts.isIdentifier(target) && + target.text === name.text && + lowerer.checker.getSymbolAtLocation(target) === symbol + ) + found = true; + } + return undefined; + }); + return found; +} diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ea5ef72ac..0f1b097c2 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1010,6 +1010,44 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b return false; } +/** The CLASS-DECLARATION twin of predeclareForwardCapture: a function + * hoisted above a local `class D` (`function make() { return new D(); }` + * reached through an earlier function) captures the class binding, which + * is in its temporal dead zone until the class statement runs. The class + * cannot be collected yet (its heritage may name bindings declared between + * here and the statement), so the binding is pre-declared as a TDZ box + * holding the class as a checked-dynamic constructor; the class statement + * later initializes it, and every reference reads it from there. */ +export function predeclareForwardClass(lowerer: Lowerer, symbol: ts.Symbol): boolean { + const decl = symbol.valueDeclaration; + if (!decl || !ts.isClassDeclaration(decl) || decl.name === undefined) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return false; + for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { + const entry = lowerer.activeStmtLists[i]!; + const idx = entry.stmts.indexOf(decl); + if (idx < 0) continue; + if (idx < entry.index || entry.ctx === lowerer.ctx || entry.ctx.tdzPredeclared.has(symbol)) + return false; + const name = decl.name.text; + const count = entry.ctx.localCounters.get(name) ?? 0; + entry.ctx.localCounters.set(name, count + 1); + const local: IrLocal = { + id: `${name}.${count}`, + name, + type: DYN, + mutable: false, + boxed: true, + tdz: true, + }; + entry.ctx.locals.push(local); + entry.frame.set(symbol, local); + entry.out.push({ kind: "varDecl", localId: local.id, init: null, loc: locOf(decl) }); + entry.ctx.tdzPredeclared.set(symbol, local); + return true; + } + return false; +} + /** The FUNCTION-DECLARATION twin of predeclareForwardCapture: JS hoists a * nested `function f() {}` to scope entry — the binding is LIVE from the * first statement, so a reference lexically above the declaration in the @@ -1529,6 +1567,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isClassDeclaration(stmt)) { if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations"); const info = lowerer.lowerClassExpressionInfo(stmt); + // An earlier hoisted function captured the class: its TDZ box exists. + const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); + const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; + if (pre) { + lowerer.ctx.tdzPredeclared.delete(symbol!); + const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN); + return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) }; + } const type: IrType = { kind: "classval", className: info.def.name }; const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true); // Static members of a function-local class run while the class value is diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 93c6246d9..83ac404d4 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,5 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; +import { moduleNamespaceObject } from "./module-namespace-elements.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; @@ -9445,6 +9446,22 @@ export class Lowerer { } if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr)) return this.divergentValue(expr, expected); + // A builtin error constructor global stored in a slot of its class value + // type (`{ Parent: Error }`): the class value itself, which a checked + // cast of the dynamic constructor value could not produce. + if ( + expected.kind === "classval" && + expr.kind === "libCall" && + expr.fn === "dyn.globalConstructor" && + expr.args[0]?.kind === "strLit" && + `%${expr.args[0].value}` === expected.className + ) { + const info = this.classes.get(expected.className); + if (info?.builtinError && info.valueCtorParams !== undefined) { + this.defineClassValueConstruct(info); + return { kind: "classRef", className: info.def.name, type: expected, loc: expr.loc }; + } + } // process.stdout/stderr into a general Writable slot (general-streams.ts). const stdioStream = stdioIntoGeneralStream(this, expr, expected); if (stdioStream) return this.coerceToExpected(stdioStream, expected); @@ -9551,6 +9568,13 @@ export class Lowerer { // constructor. `require("events")` is the EventEmitter class itself, // so its module binding is too (`ReleaseEmitter = EventEmitter` over // `const EventEmitter = require("events")`). + if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) { + const object = moduleNamespaceObject( + this, + expr as IrExpr & { type: IrType & { kind: "moduleNs" } }, + ); + if (object) return object; + } if ( expr.type.kind === "moduleNs" && expr.type.moduleId === "builtin:events" && diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index d2425750b..df40573da 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -1,6 +1,13 @@ import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; -import { DYN, STRING, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js"; +import { + DYN, + STRING, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import { locOf } from "../program.js"; import { newFnCtx, type Lowerer } from "./lowerer.js"; @@ -88,3 +95,76 @@ export function lowerModuleNamespaceElement( }; return { kind: "call", callee: name, args: [receiver, text], type: DYN, loc }; } + +/** A module namespace taken as an untyped value (`{ ...ns }`, `{ iso: ns }` + * in JavaScript): an object holding each value export, keyed in the + * namespace's sorted key order. The copy is exact while every export is a + * binding that never changes after initialization (const, function or + * class); null for other namespaces, which keep their fence. */ +export function moduleNamespaceObject( + lowerer: Lowerer, + ns: IrExpr & { type: IrType & { kind: "moduleNs" } }, +): IrExpr | null { + const source = lowerer.sourceFileOfModuleNamespace(ns.type); + if (!source) return null; + const symbol = lowerer.checker.getSymbolAtLocation(source); + if (!symbol) return null; + const exports: { key: string; identifier: ts.Identifier }[] = []; + let exact = true; + symbol.getExports().forEach((exported, key) => { + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + if (!(target.flags & ts.SymbolFlags.Value)) return; + const declaration = target.valueDeclaration; + const identifier = declaration?.name as ts.Node | undefined; + const immutable = + declaration !== undefined && + (ts.isFunctionDeclaration(declaration) || + ts.isClassDeclaration(declaration) || + (ts.isVariableDeclaration(declaration) && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0)); + if (!identifier || !ts.isIdentifier(identifier) || !immutable) { + exact = false; + return; + } + exports.push({ key: String(key), identifier }); + }); + if (!exact) return null; + exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const name = `%module.object:${ns.type.moduleId}`; + const loc = ns.loc; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + const previousClass = lowerer.currentClass; + lowerer.currentClass = null; + lowerer.fnStack.push(context); + try { + const fields = exports.map(({ key, identifier }) => ({ + key: strLit(key, loc), + value: lowerer.lowerExprExpecting(identifier, DYN), + })); + if (fields.some((field) => field.value.type.kind !== "dyn")) return null; + const fn: IrFunction = { + name, + params: [], + returnType: DYN, + locals: [...context.locals], + body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }], + loc, + }; + lowerer.liftedFns.push(fn); + } finally { + lowerer.fnStack.pop(); + lowerer.currentClass = previousClass; + } + } + const call: IrExpr = { kind: "call", callee: name, args: [], type: DYN, loc }; + if (ns.kind === "moduleNsRef") return call; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: ns, loc }], + result: call, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json new file mode 100644 index 000000000..efb9dd725 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7480-builtin-constructor-in.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json new file mode 100644 index 000000000..d0d390662 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7481-class-runtime-heritage-object-error.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json new file mode 100644 index 000000000..d1d97ba4f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7482-define-property-function-class-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json new file mode 100644 index 000000000..c3fa9964f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7483-constructor-factory-traits.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json new file mode 100644 index 000000000..515b9bf97 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7484-hoisted-function-captures-later-class.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json new file mode 100644 index 000000000..aa1affb3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7485-global-object-own-properties.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json new file mode 100644 index 000000000..0cf87c443 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7486-prototype-accessor-string-method-names.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json new file mode 100644 index 000000000..0c768ef66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7487-namespace-spread-untyped/shapes.js", + "/tests/corpus/7487-namespace-spread-untyped/extra.js", + "/tests/corpus/7487-namespace-spread-untyped/main.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 74b710dc9..63d7cb1b7 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -863,6 +863,7 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators); * for-of protocol reads it. +1, or NULL with the error pending. */ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); +void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when @@ -1270,6 +1271,7 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); +int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee); /* JS String() over a dyn value (arrays join, [object Object], the error * encoding renders "name: message"); borrowed, result +1, never throws. */ ScrStr *scr_dyn_display(const ScrDyn *d); diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 2f947048a..0647a65c0 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -1581,6 +1581,8 @@ bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; } + int hooked = scr_dyn_has_instance_hook(value, callee); + if (hooked >= 0) return hooked == 1; ScrDyn *prototype = bc_get(callee, "prototype"); if (!prototype) return false; bool found = false; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cf658313f..57c9e4df4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -189,6 +189,19 @@ void scr_dyn_install_async_iterator_symbol(ScrDyn *key) { scr_atexit(scr_async_iterator_symbol_cleanup); } +/* Symbol.hasInstance, installed when the program first evaluates it: a + * function can only define a hook after reading the symbol. */ +static SCR_TL ScrDyn *scr_has_instance_symbol; +static void scr_has_instance_symbol_cleanup(void) { + scr_dyn_release(scr_has_instance_symbol); + scr_has_instance_symbol = NULL; +} +void scr_dyn_install_has_instance_symbol(ScrDyn *key) { + if (scr_has_instance_symbol) return; + scr_has_instance_symbol = scr_dyn_retain(key); + scr_atexit(scr_has_instance_symbol_cleanup); +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -3983,12 +3996,49 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD /* `value instanceof callee` for an untyped constructor value. `refusal`, when * given, is the site's SC1090 message for a runtime constructor with no * instance test. */ +/* A function whose own-property table defines Symbol.hasInstance + * (`Object.defineProperty(F, Symbol.hasInstance, { value })`): 1 or 0 by the + * truthiness of the method's answer with `this` bound to F, -1 when F + * defines none, or 0 with the exception pending. Functions without a table + * use the ordinary prototype test. */ +int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) { + if (!scr_has_instance_symbol || callee->kind != SCR_DYN_FUNC || !scr_closure_identity(callee->v.fn.clo)->props) + return -1; + ScrDyn *hook = scr_dyn_symbol_key_get(callee, scr_has_instance_symbol, true); + if (scr_exc_pending()) { + scr_dyn_release(hook); + return 0; + } + if (!hook || hook->kind != SCR_DYN_FUNC) { + scr_dyn_release(hook); + return -1; + } + scr_dyn_this_push_dyn((ScrDyn *)callee); + ScrDyn *argument = (ScrDyn *)value; + ScrDyn *result = scr_dyn_call(hook, &argument, 1, "Symbol.hasInstance"); + scr_dyn_this_pop(); + scr_dyn_release(hook); + if (!result) return 0; + bool truthy = scr_dyn_truthy(result); + scr_dyn_release(result); + return truthy ? 1 : 0; +} + bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return false; } + int hooked = scr_dyn_has_instance_hook(value, callee); + if (hooked >= 0) return hooked == 1; + /* An error constructor held as a value (`inst instanceof params.Parent` + * with Parent === Error): the error kind's interval, and for Error itself + * objects inheriting Error.prototype. */ + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + if (!scr_error_ctor_values[i] || callee->v.fn.clo != scr_error_ctor_values[i]->v.fn.clo) continue; + return scr_dyn_err_instanceof(value, (double)i) || (i == 0 && scr_dyn_inherits_error_prototype(value)); + } if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { @@ -5021,7 +5071,43 @@ ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { return scr_exc_pending() ? NULL : scr_dyn_retain(constructor); } +/* Defines `key` on target as a writable, configurable, non-enumerable data + * property holding value (borrowed), as Error's constructor installs its + * message and cause. */ +static void scr_dyn_define_hidden(ScrDyn *target, const ScrStr *key, ScrDyn *value) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(value)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *defined = scr_dyn_define_property(target, name, descriptor); + scr_dyn_release(name); + scr_dyn_release(descriptor); + scr_dyn_release(defined); +} + +/* A builtin constructor a class expression's runtime heritage can name: + * 1 for Object, 2 for Error, 0 otherwise. */ +static int scr_dyn_builtin_base_kind(const ScrDyn *constructor) { + if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || !constructor->v.fn.sig || + !constructor->v.fn.name || strcmp(constructor->v.fn.sig, "native:constructor") != 0) + return 0; + if (strcmp(constructor->v.fn.name, "Object") == 0) return 1; + if (strcmp(constructor->v.fn.name, "Error") == 0 && constructor->v.fn.clo->fn == (void *)scr_error_ctor_names[0]) + return 2; + return 0; +} + ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { + /* `class extends (Base ?? Object)`: Object's instances are ordinary + * objects, laid out like a class without heritage (undefined); Error's + * inherit Error.prototype. */ + switch (scr_dyn_builtin_base_kind(constructor)) { + case 1: return scr_dyn_retain(scr_dyn_undefined()); + case 2: return scr_dyn_error_prototype(); + default: break; + } if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || scr_closure_identity(constructor->v.fn.clo)->function_kind != 4) { static const char message[] = "Class extends value is not a native ordinary constructor"; @@ -5038,6 +5124,48 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { } void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *arguments) { + switch (scr_dyn_builtin_base_kind(constructor)) { + case 1: + /* super() through Object keeps the receiver the derived constructor + * created (OrdinaryCreateFromConstructor). */ + return; + case 2: { + /* super(message, options) through Error: the message and cause become + * non-enumerable own properties, then the stack (its text alone, as for + * scriptc's other errors). */ + size_t argc = arguments->v.arr.len; + ScrDyn *const *argv = arguments->v.arr.items; + if (argc > 0 && argv[0]->kind != SCR_DYN_UNDEF) { + ScrStr *text = scr_dyn_string_coerce_js(argv[0]); + if (!text) return; + ScrDyn *value = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new("message", 7); + scr_dyn_define_hidden(receiver, key, value); + scr_str_release(key); + scr_dyn_release(value); + if (scr_exc_pending()) return; + } + if (argc > 1 && argv[1]->kind == SCR_DYN_OBJ && scr_dyn_find_entry(argv[1], "cause", 5)) { + ScrDyn *cause = scr_dyn_obj_get(argv[1], "cause", 5); + ScrStr *key = scr_str_new("cause", 5); + scr_dyn_define_hidden(receiver, key, cause ? cause : scr_dyn_undefined()); + scr_str_release(key); + if (scr_exc_pending()) return; + } + ScrStr *text = scr_dyn_error_string(receiver); + if (!text) return; + ScrDyn *stack = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new("stack", 5); + scr_dyn_define_hidden(receiver, key, stack); + scr_str_release(key); + scr_dyn_release(stack); + return; + } + default: + break; + } scr_dyn_this_push_dyn(receiver); ScrDyn *result = scr_dyn_call(constructor, arguments->v.arr.items, arguments->v.arr.len, "super"); scr_dyn_this_pop(); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 304f2544f..647c31dc3 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -85,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_iterator_symbol(key); scr_dyn_release(key); } + if (name->len == 11 && !memcmp(name->data, "hasInstance", 11)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_has_instance_symbol(key); + scr_dyn_release(key); + } if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_async_iterator_symbol(key); diff --git a/tests/corpus/7480-builtin-constructor-in.js b/tests/corpus/7480-builtin-constructor-in.js new file mode 100644 index 000000000..781a00105 --- /dev/null +++ b/tests/corpus/7480-builtin-constructor-in.js @@ -0,0 +1,9 @@ +// Feature probes on builtin constructor globals answer like Node. +const E = "captureStackTrace" in Error ? Error : null; +console.log(E === Error, "stackTraceLimit" in Error, "nope" in Error); +console.log("captureStackTrace" in TypeError, "keys" in Object, "fromAsync" in Array, "withResolvers" in Promise); +console.log("call" in Map, "hasOwnProperty" in Set, "groupBy" in Map, "groupBy" in Set, "now" in Date); +const capture = "captureStackTrace" in Error ? Error.captureStackTrace : () => {}; +const target = {}; +capture(target); +console.log(typeof target.stack); diff --git a/tests/corpus/7481-class-runtime-heritage-object-error.js b/tests/corpus/7481-class-runtime-heritage-object-error.js new file mode 100644 index 000000000..13d59dc63 --- /dev/null +++ b/tests/corpus/7481-class-runtime-heritage-object-error.js @@ -0,0 +1,24 @@ +function make(name, params) { + const Parent = (params && params.Parent) || Object; + class Definition extends Parent { + describe() { + return `${name}:${this.kind}`; + } + } + return Definition; +} +const A = make("A"); +const a = new A(); +a.kind = "plain"; +console.log(typeof a, a instanceof A, a.describe(), JSON.stringify(a), Object.keys(a)); +const E = make("E", { Parent: Error }); +const e = new E("boom"); +e.kind = "error"; +console.log(e instanceof E, e.message, typeof e.stack, e.describe(), Object.keys(e), String(e)); +const Parent = E; +console.log(e instanceof Parent, new E() .message === "", Object.prototype.hasOwnProperty.call(new E(), "message")); +try { + throw new E("thrown"); +} catch (err) { + console.log(err instanceof E, err.message); +} diff --git a/tests/corpus/7482-define-property-function-class-values.js b/tests/corpus/7482-define-property-function-class-values.js new file mode 100644 index 000000000..7a682adab --- /dev/null +++ b/tests/corpus/7482-define-property-function-class-values.js @@ -0,0 +1,35 @@ +const desc = { value: undefined, enumerable: false }; +function factory(name) { + class Definition {} + Object.defineProperty(Definition, "name", { value: name }); + function init(inst, def) { + if (!inst._meta) { + desc.value = { def, traits: new Set() }; + try { + Object.defineProperty(inst, "_meta", desc); + } finally { + desc.value = undefined; + } + } + inst._meta.traits.add(name); + } + function _(def) { + const inst = this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, Symbol.hasInstance, { + value: (inst) => !!(inst && inst._meta && inst._meta.traits.has(name)), + }); + Object.defineProperty(_, "name", { value: name }); + return { ctor: _, Definition }; +} +const { ctor: Thing, Definition } = factory("Thing"); +console.log(Definition.name, Thing.name, typeof Thing.init); +const t = new Thing({ a: 1 }); +console.log(Object.keys(t), JSON.stringify(t), t._meta.def.a, [...t._meta.traits]); +console.log(t instanceof Thing, {} instanceof Thing); +const plain = {}; +Thing.init(plain, { b: 2 }); +console.log(plain instanceof Thing, Object.keys(plain), plain._meta.def.b); diff --git a/tests/corpus/7483-constructor-factory-traits.js b/tests/corpus/7483-constructor-factory-traits.js new file mode 100644 index 000000000..e022f0672 --- /dev/null +++ b/tests/corpus/7483-constructor-factory-traits.js @@ -0,0 +1,58 @@ +const _desc = { value: undefined, enumerable: false }; +function $constructor(name, initializer, params) { + const proto = {}; + function Internals(def) { + this.def = def; + this.constr = _; + this.traits = new Set(); + } + Internals.prototype = proto; + function init(inst, def) { + if (!inst._zod) { + _desc.value = new Internals(def); + try { + Object.defineProperty(inst, "_zod", _desc); + } finally { + _desc.value = undefined; + } + } else if (inst._zod.traits.has(name)) { + return; + } + inst._zod.traits.add(name); + initializer(inst, def); + } + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + init(inst, def); + return inst; + } + Object.defineProperty(_, "init", { value: init }); + Object.defineProperty(_, Symbol.hasInstance, { + value: (inst) => { + if (params?.Parent && inst instanceof params.Parent) return true; + return inst?._zod?.traits?.has(name); + }, + }); + Object.defineProperty(_, "name", { value: name }); + return _; +} +const $Base = $constructor("$Base", (inst, def) => { + inst.kind = def.kind; + inst._zod.parse = (v) => (typeof v === def.kind ? { value: v } : { issues: [`expected ${def.kind}`] }); +}); +const $Str = $constructor("$Str", (inst, def) => { + $Base.init(inst, def); + inst.min = def.min; +}); +const $Err = $constructor("$Err", (inst, def) => { + inst.issues = def; + inst.message = JSON.stringify(def); +}, { Parent: Error }); +const s = new $Str({ kind: "string", min: 2 }); +console.log(s.kind, s.min, [...s._zod.traits], s instanceof $Str, s instanceof $Base, Object.keys(s)); +console.log(JSON.stringify(s._zod.parse("ab")), JSON.stringify(s._zod.parse(3))); +const e = new $Err(["bad"]); +console.log(e instanceof $Err, e.issues, e.message, $Str.name, $Err.name); diff --git a/tests/corpus/7484-hoisted-function-captures-later-class.js b/tests/corpus/7484-hoisted-function-captures-later-class.js new file mode 100644 index 000000000..2272882af --- /dev/null +++ b/tests/corpus/7484-hoisted-function-captures-later-class.js @@ -0,0 +1,23 @@ +function f(params) { + function early() { + return make; + } + const Parent = params?.Parent ?? Object; + class D extends Parent {} + function make() { + return new D(); + } + return early()(); +} +console.log(typeof f(), typeof f({ Parent: Error })); +function g() { + function early() { + return make; + } + class E {} + function make() { + return new E(); + } + return early()(); +} +console.log(typeof g()); diff --git a/tests/corpus/7485-global-object-own-properties.js b/tests/corpus/7485-global-object-own-properties.js new file mode 100644 index 000000000..a9a72be18 --- /dev/null +++ b/tests/corpus/7485-global-object-own-properties.js @@ -0,0 +1,22 @@ +var _a; +globalThis.__direct = { n: 1 }; +console.log(typeof globalThis.__direct, globalThis.__direct.n); + +(_a = globalThis).__alias ?? (_a.__alias = { n: 2 }); +console.log(typeof globalThis.__alias); +const g = globalThis; +g.__viaConst = { n: 3 }; +console.log(typeof globalThis.__viaConst); +globalThis.__nullish ??= { n: 4 }; +console.log(typeof globalThis.__nullish); + +(_a = globalThis).__demo_config ?? (_a.__demo_config = {}); +const globalConfig = globalThis.__demo_config; +function config(next) { + if (next) Object.assign(globalConfig, next); + return globalConfig; +} +config({ localeError: (issue) => `bad ${issue}` }); +console.log(typeof globalConfig, typeof globalConfig.localeError, config().localeError("x")); +const pp = globalThis.__demo_config?.postProcessor; +console.log(pp === undefined, globalThis.__demo_config === globalConfig); diff --git a/tests/corpus/7486-prototype-accessor-string-method-names.js b/tests/corpus/7486-prototype-accessor-string-method-names.js new file mode 100644 index 000000000..9920717ea --- /dev/null +++ b/tests/corpus/7486-prototype-accessor-string-method-names.js @@ -0,0 +1,53 @@ +function own(inst, key, value, enumerable = true) { + Object.defineProperty(inst, key, { configurable: true, writable: true, enumerable, value }); + return value; +} +function defineBound(proto, key, fn) { + Object.defineProperty(proto, key, { + configurable: true, + get() { + return this == null ? fn : own(this, key, fn.bind(this)); + }, + set(value) { + own(this, key, value); + }, + }); +} +function members(proto, table) { + for (const key in table) { + const desc = Object.getOwnPropertyDescriptor(table, key); + if (desc.get) Object.defineProperty(proto, key, { ...desc, enumerable: false }); + else defineBound(proto, key, desc.value); + } +} +function Schema(def) { + this.def = def; +} +let installed = false; +function make(def) { + const inst = new Schema(def); + if (!installed) { + installed = true; + members(Object.getPrototypeOf(inst), { + min(n) { return make({ ...this.def, min: n }); }, + max(n) { return make({ ...this.def, max: n }); }, + trim() { return make({ ...this.def, trim: true }); }, + get size() { return Object.keys(this.def).length; }, + }); + } + return inst; +} +const s = make({ kind: "string" }).min(1).max(200).trim(); +console.log(JSON.stringify(s.def), s.size, Object.keys(s), typeof s.trim); +const detached = make({ kind: "x" }).max; +console.log(JSON.stringify(detached(3).def)); +const proto2 = {}; +Object.defineProperty(proto2, "aaa", { configurable: true, get() { return () => "custom-aaa"; } }); +Object.defineProperty(proto2, "trim", { configurable: true, get() { return () => "custom-trim"; } }); +Object.defineProperty(proto2, "trimx", { configurable: true, get() { return () => "custom-trimx"; } }); +Object.defineProperty(proto2, "slice", { configurable: true, get() { return () => "custom-slice"; } }); +const o2 = Object.create(proto2); +console.log(o2.aaa()); +console.log(o2.trimx()); +console.log(o2.slice()); +console.log(o2.trim()); diff --git a/tests/corpus/7487-namespace-spread-untyped/extra.js b/tests/corpus/7487-namespace-spread-untyped/extra.js new file mode 100644 index 000000000..8c62c2dd0 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/extra.js @@ -0,0 +1,2 @@ +export const tag = (s) => `<${s}>`; +export function zeta() { return "z"; } diff --git a/tests/corpus/7487-namespace-spread-untyped/main.js b/tests/corpus/7487-namespace-spread-untyped/main.js new file mode 100644 index 000000000..021c00ac9 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/main.js @@ -0,0 +1,11 @@ +import * as shapes from "./shapes.js"; +import * as extra from "./extra.js"; +// A namespace spread into an untyped object (zod's local +// `const z = { ..._schemas, ..._checks, iso: _iso }`). +function untyped(value) { + return value; +} +const z = untyped({ ...shapes, ...extra, extra }); +console.log(Object.keys(z).join(",")); +for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2)); +console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size); diff --git a/tests/corpus/7487-namespace-spread-untyped/shapes.js b/tests/corpus/7487-namespace-spread-untyped/shapes.js new file mode 100644 index 000000000..a8b5cbfc2 --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/shapes.js @@ -0,0 +1,4 @@ +export const VERSION = "1.2"; +export function circle(r) { return { kind: "circle", area: Math.PI * r * r }; } +export function square(s) { return { kind: "square", area: s * s }; } +export class Box { constructor(size) { this.size = size; } } diff --git a/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7485-global-object-own-properties.js.json b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } diff --git a/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } From 982572c173d8f6c2fb8b75512b46ee9a1fb6601a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 17:47:20 -0700 Subject: [PATCH 02/30] Dispatch splice on arrays of unknown elements Arrays the checker types with unknown elements are stored checked-dynamic, and their methods dispatch on the runtime array. splice was missing from that dispatch table, so unknown[] receivers refused it, including generic bodies instantiated at unknown (pool.splice(i, 1) in a sample helper gets such an instantiation once a program has a dynamic tier). The runtime's dynInvoke already implements splice; the table now names it, and arrays of unknown elements take the dispatch from the array-method lowering. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/containers/array-methods.ts | 15 +++++++++++++++ .../compiler/src/frontend/lowering/lower-calls.ts | 1 + .../corpus/7402-unknown-array-methods.ts.json | 6 ++++++ tests/corpus/7402-unknown-array-methods.ts | 14 ++++++++++++++ .../7402-unknown-array-methods.ts.json | 1 + 5 files changed, 37 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json create mode 100644 tests/corpus/7402-unknown-array-methods.ts create mode 100644 tests/harness/corpus-costs/7402-unknown-array-methods.ts.json diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda..3ab2e84e2 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -361,6 +361,21 @@ export function lowerArrayMethodCall( if (probe.type.kind === "array") { receiverIr = probe.type; probedUntyped = true; + } else if ( + probe.type.kind === "dyn" && + !isJsSourceFile(ts.getSourceFileOfNode(access)) && + lowerer.isStdlibMember(access) + ) { + // An `unknown[]` (or `any[]`) array stored checked-dynamic — a + // generic body instantiated at `unknown`, a spread of unknown + // elements: the method runs on the runtime array (dynInvoke, JS + // exact), and a typed result is checked like any member read. + const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, probe, true); + if (!dispatched) return null; + const result = lowerer.mapTypeOf(lowerer.typeOf(call)); + return result && result.kind !== "void" && result.kind !== "dyn" + ? lowerer.coerceInto(call, dispatched, result) + : dispatched; } } if (receiverIr?.kind === "union") { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c0406e77b..246958d54 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4243,6 +4243,7 @@ const DYN_DISPATCH_METHODS = new Set([ "shift", "unshift", "slice", + "splice", "at", "indexOf", "lastIndexOf", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json new file mode 100644 index 000000000..db33da675 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7402-unknown-array-methods.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7402-unknown-array-methods.ts b/tests/corpus/7402-unknown-array-methods.ts new file mode 100644 index 000000000..e374dd40f --- /dev/null +++ b/tests/corpus/7402-unknown-array-methods.ts @@ -0,0 +1,14 @@ +// Methods on arrays of unknown elements, which are stored checked-dynamic: +// direct `unknown[]` bindings and generic bodies instantiated at `unknown`. +const values: unknown[] = [1, "two", 3, { four: 4 }]; +const removed = values.splice(1, 2); +console.log(values, removed, removed.length); +values.splice(1, 0, "inserted", null); +console.log(values, values.indexOf(null), values.includes("inserted")); +console.log(values.slice(1).reverse(), values.join("|")); + +function takeAt(items: readonly T[], index: number): T[] { + const pool = [...items]; + return pool.splice(index, 1); +} +console.log(takeAt([10, 20, 30], 1), takeAt([true, "x", 7], 2)); diff --git a/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json new file mode 100644 index 000000000..9eb118813 --- /dev/null +++ b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 20000 } From 17bef8c03b505a5bbb934f06cd6f2a35b190eede Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:54:55 -0700 Subject: [PATCH 03/30] Type user code from npm package declarations at a checked, identity-preserving boundary Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/dyn.ts | 32 +- packages/compiler/src/backend/llvm/emitter.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../compiler/src/frontend/decl-projection.ts | 735 ++++ .../frontend/lowering/coercions/structural.ts | 17 + .../lowering/expressions/narrowing.ts | 74 + .../frontend/lowering/key-order-witness.ts | 9 +- .../compiler/src/frontend/lowering/lowerer.ts | 19 +- packages/compiler/src/frontend/program.ts | 148 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/ir.ts | 13 + packages/compiler/src/ir/validate.ts | 2 + packages/runtime/src/scr_dyn.h | 5 + packages/runtime/src/scr_json.c | 207 + .../fixtures/npm/cases/zod3-boundary/main.ts | 83 + .../zod3-boundary/node_modules/zod/LICENSE | 21 + .../node_modules/zod/dist/esm/index.js | 3 + .../node_modules/zod/dist/esm/package.json | 3 + .../node_modules/zod/dist/esm/v3/ZodError.js | 132 + .../node_modules/zod/dist/esm/v3/errors.js | 9 + .../node_modules/zod/dist/esm/v3/external.js | 6 + .../zod/dist/esm/v3/helpers/enumUtil.js | 1 + .../zod/dist/esm/v3/helpers/errorUtil.js | 6 + .../zod/dist/esm/v3/helpers/parseUtil.js | 109 + .../zod/dist/esm/v3/helpers/partialUtil.js | 1 + .../zod/dist/esm/v3/helpers/typeAliases.js | 1 + .../zod/dist/esm/v3/helpers/util.js | 133 + .../node_modules/zod/dist/esm/v3/index.js | 4 + .../zod/dist/esm/v3/locales/en.js | 107 + .../zod/dist/esm/v3/standard-schema.js | 1 + .../node_modules/zod/dist/esm/v3/types.js | 3691 +++++++++++++++++ .../node_modules/zod/dist/types/index.d.ts | 3 + .../zod/dist/types/v3/ZodError.d.ts | 164 + .../zod/dist/types/v3/errors.d.ts | 5 + .../zod/dist/types/v3/external.d.ts | 6 + .../zod/dist/types/v3/helpers/enumUtil.d.ts | 8 + .../zod/dist/types/v3/helpers/errorUtil.d.ts | 9 + .../zod/dist/types/v3/helpers/parseUtil.d.ts | 78 + .../dist/types/v3/helpers/partialUtil.d.ts | 8 + .../dist/types/v3/helpers/typeAliases.d.ts | 2 + .../zod/dist/types/v3/helpers/util.d.ts | 85 + .../node_modules/zod/dist/types/v3/index.d.ts | 4 + .../zod/dist/types/v3/locales/en.d.ts | 3 + .../zod/dist/types/v3/standard-schema.d.ts | 102 + .../node_modules/zod/dist/types/v3/types.d.ts | 1031 +++++ .../node_modules/zod/package.json | 120 + .../npm/cases/zod3-boundary/package.json | 1 + 48 files changed, 7196 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/src/frontend/decl-projection.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/main.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/cases/zod3-boundary/package.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..72e6c6c9e 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -153,6 +153,8 @@ export interface DynHost extends WalkerHost { /** See moduleUsesDynBuiltinStream. */ usesDynBuiltinStream?(): boolean; liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string; + /** The module has declaration-typed boundary casts (IrModule.declBoundary). */ + declBoundary?(): boolean; dynPromiseAdapter(t: IrType): string; isErrorClass(className: string): boolean; classSubtypes(className: string): readonly string[]; @@ -4526,6 +4528,31 @@ export class LlDyn { /** The dyn argument spelling of one static value: dyn passes through * (+1), funcs box anonymously, everything else rides toDyn. `expr` is * BORROWED in every arm. Emits into the given builder. */ + /** A live capsule of a record or array value (+1; the value is + * borrowed), as `dynFrom` with `liveRef` builds it; null for other + * types. Closures boxed in a declaration-typed boundary program return + * their records and arrays this way. */ + private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null { + const host = this.host; + if (t.kind === "union") { + const arms = host.unionsById.get(t.unionId)?.arms ?? []; + if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null; + const boxed = B.tmp(); + B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`); + return boxed; + } + if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null; + const key = typeKey(t); + const adapter = host.liveDynRefAdapter(t); + const rc = vAdapters(host, t); + const boxed = B.tmp(); + B.line( + `${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`, + ); + for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line); + return boxed; + } + private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string { if (t.kind === "dyn") return this.retainDyn(B, expr); if (t.kind === "func") { @@ -4689,7 +4716,10 @@ export class LlDyn { const r = B.tmp(); B.line(`${r} = call ${retTy} ${fn}(${callArgs})`); this.pendingBail(B, "dfkr", () => {}, "ptr null"); - const out = this.toDynExpr(B, t.ret, r); + const out = + this.host.declBoundary?.() === true + ? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r)) + : this.toDynExpr(B, t.ret, r); if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`); B.terminate(`ret ptr ${out}`); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..8e14541af 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1597,6 +1597,7 @@ export class LlEmitter { liveDynRefAdapter: (type) => this.liveDynRefAdapter(type), usesDynBuiltinStream: () => this.usesDynBuiltinStream, liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type), + declBoundary: () => this.mod.declBoundary === true, dynPromiseAdapter: (type) => this.dynPromiseAdapter(type), isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error", classRootOf: (name) => this.classMeta.get(name)?.root.def.name, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..ecd3d3b5b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1570,6 +1570,7 @@ export const LIB_FN_SYMS: Record = { "dyn.objValueList": "scr_dyn_obj_value_list", "dyn.objEntries": "scr_dyn_obj_entries", "dyn.structuredClone": "scr_structured_clone", + "dyn.declAdopt": "scr_dyn_decl_adopt", "dyn.cloneMissing": "scr_structured_clone_missing", "dyn.cloneTransferFail": "scr_structured_clone_transfer_fail", "dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with", diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts new file mode 100644 index 000000000..923bfb428 --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -0,0 +1,735 @@ +/* The declaration-typed boundary's projection pass. + * + * An opted-in npm package compiles from its JavaScript, and the program the + * lowering compiles types it from that JavaScript: where the package's + * published declarations are richer (zod's `z.infer`), user code sees `any`. + * This pass asks the DECLARATION world (the same entry checked against the + * packages' declarations) for the types of those user-code positions. Where + * the declared type is pure data, it is printed as structural TypeScript into + * a generated global declaration file, and the user file is rewritten in the + * lowering world: + * + * - type positions that name package types (`z.infer`) name the + * projected alias instead; + * - package-produced expressions (`S.parse(x)`, `r.data`) are wrapped in + * `( as __scriptc_decl_N)`, the boundary cast; + * - untyped parameters of user callbacks the package calls gain the alias. + * + * The lowering world is then rebuilt from the rewritten text, so the + * checker's own inference carries the projected types through the rest of + * the user code, and the lowering sees an ordinary checked cast at each + * crossing (lowered with `adopt`, see narrowing.ts). No edit adds or removes + * a line break, so positions keep their lines. IR records are structural, + * so a printed type interns to the same shape as any equal type. + * + * Only programs whose user code has such positions are rewritten; every + * other program is untouched (the zero-cost rule). See + * scriptc-perf/results/design-decl-boundary/DESIGN.md. */ + +import * as ts from "./ts7/adapter.js"; + +/** Alias names of projected types start with this prefix; the lowering + * recognizes boundary casts and parameters by it. */ +export const DECL_BOUNDARY_PREFIX = "__scriptc_decl_"; + +/** The generated declaration file's base name. */ +export const DECL_BOUNDARY_FILE = "__scriptc_decl_boundary__.d.ts"; + +export function isDeclBoundaryFile(fileName: string): boolean { + return fileName.split("\\").join("/").endsWith(`/${DECL_BOUNDARY_FILE}`); +} + +export function isDeclBoundaryTypeName(name: string): boolean { + return name.startsWith(DECL_BOUNDARY_PREFIX); +} + +/** True for `T` in `x as T` / `(p: T) =>` when T names a projected alias. */ +export function isDeclBoundaryTypeNode(node: ts.TypeNode | undefined): boolean { + return ( + node !== undefined && + ts.isTypeReferenceNode(node) && + ts.isIdentifier(node.typeName) && + isDeclBoundaryTypeName(node.typeName.text) && + node.typeArguments === undefined + ); +} + +interface Edit { + start: number; + end: number; + text: string; + /** Tie-break among edits at one position (lower first). */ + rank?: number; +} + +export interface DeclProjection { + /** Rewritten text per user file. */ + files: Map; + /** The generated declaration file (path and text). */ + declFile: string; + declText: string; + /** Human-readable plan (SCRIPTC_DEBUG_DECL_BOUNDARY). */ + plan: string[]; +} + +export interface DeclProjectionInput { + jsProgram: ts.Program; + declProgram: ts.Program; + /** User source files to consider (TypeScript, outside node_modules). */ + userFiles: readonly string[]; + /** Directory of the generated declaration file. */ + outDir: string; +} + +function isPackageDeclarationFile(sf: ts.SourceFile | undefined): boolean { + if (sf === undefined || !sf.isDeclarationFile) return false; + const f = sf.fileName.split("\\").join("/"); + // The TypeScript standard library ships inside node_modules too. + if (/(^|\/)lib(\.[\w.-]+)?\.d\.ts$/.test(f) || f.startsWith("bundled:")) return false; + return ( + f.includes("/node_modules/") && + !f.includes("/node_modules/@types/node/") && + !f.includes("/node_modules/typescript/") && + !f.includes("/node_modules/@typescript/") + ); +} + +function declaredInPackage(symbol: ts.Symbol | undefined): boolean { + if (symbol === undefined) return false; + return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d))); +} + +function declaredInUserFile(symbol: ts.Symbol | undefined): boolean { + if (symbol === undefined) return false; + const decls = symbol.declarations; + return ( + decls.length > 0 && + decls.every((d) => { + const sf = ts.getSourceFileOfNode(d); + return !sf.isDeclarationFile && !sf.fileName.split("\\").join("/").includes("/node_modules/"); + }) + ); +} + +const ANYISH = ts.TypeFlags.Any | ts.TypeFlags.Unknown; +const READONLY_CHECK_FLAG = 8; + +/** Prints declaration-world types as structural TypeScript, or answers null + * when a type is not pure data (DESIGN.md §4.3). */ +class TypePrinter { + readonly aliases = new Map(); + readonly declarations: string[] = []; + private readonly inProgress = new Map(); + + constructor(private readonly checker: ts.TypeChecker) {} + + /** The alias naming a projectable type, or null. Top-level any/unknown + * never projects. */ + project(type: ts.Type): string | null { + if (type.flags & ANYISH) return null; + const text = this.print(type, 0); + if (text === null || text === "unknown") return null; + let name = this.aliases.get(text); + if (name === undefined) { + name = `${DECL_BOUNDARY_PREFIX}${this.aliases.size}`; + this.aliases.set(text, name); + this.declarations.push(`type ${name} = ${text};`); + } + return name; + } + + private print(type: ts.Type, depth: number): string | null { + if (depth > 24) return null; + const f = type.flags; + if (f & ANYISH) return "unknown"; + if (f & ts.TypeFlags.String) return "string"; + if (f & ts.TypeFlags.Number) return "number"; + if (f & ts.TypeFlags.Boolean) return "boolean"; + if (f & ts.TypeFlags.Null) return "null"; + if (f & ts.TypeFlags.Undefined) return "undefined"; + if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null; + if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false"; + if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value)); + if (f & ts.TypeFlags.NumberLiteral) { + const v = Number(type.value); + return Number.isFinite(v) ? String(v) : null; + } + if (type.isUnionType()) { + const parts: string[] = []; + for (const arm of type.getTypes()) { + const p = this.print(arm, depth + 1); + if (p === null || p === "unknown") return null; + if (!parts.includes(p)) parts.push(p); + } + // `true | false` reads better as boolean, and maps the same. + const t = parts.indexOf("true"); + const fl = parts.indexOf("false"); + if (t >= 0 && fl >= 0) { + parts.splice(Math.max(t, fl), 1); + parts.splice(Math.min(t, fl), 1, "boolean"); + } + return parts.map((p) => (/=>/.test(p) ? `(${p})` : p)).join(" | "); + } + if (type.isIntersectionType()) { + // Brands: drop members that only carry unique-symbol keys. + const kept = type.getTypes().filter((m) => !this.isBrandOnly(m)); + if (kept.length === 1) return this.print(kept[0]!, depth + 1); + if (kept.length === 0) return null; + if (!kept.every((m) => (m.flags & ts.TypeFlags.Object) !== 0)) return null; + return this.printObject(type, depth); + } + if (f & ts.TypeFlags.Object) return this.printObject(type, depth); + return null; + } + + private isBrandOnly(type: ts.Type): boolean { + if (!(type.flags & ts.TypeFlags.Object) || type.isIntersectionType()) return false; + const props = this.checker.getPropertiesOfType(type); + return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@")); + } + + private printObject(type: ts.Type, depth: number): string | null { + const checker = this.checker; + if (checker.isArrayType(type)) { + const [elem] = checker.getTypeArguments(type); + if (elem === undefined) return null; + const e = this.print(elem, depth + 1); + if (e === null) return null; + const ro = type.getSymbol()?.escapedName === "ReadonlyArray" ? "readonly " : ""; + return `${ro}(${e})[]`; + } + if (checker.isTupleType(type)) { + const target = type.getTarget() ?? type; + const flags = target.elementFlags ?? []; + if (flags.some((fl) => fl !== ts.ElementFlags.Required)) return null; + const elems = checker.getTypeArguments(type).slice(0, flags.length); + const out: string[] = []; + for (const e of elems) { + const p = this.print(e, depth + 1); + if (p === null) return null; + out.push(p); + } + return `${target.readonly === true ? "readonly " : ""}[${out.join(", ")}]`; + } + const symbol = type.getSymbol(); + if (symbol !== undefined) { + if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method)) + return null; + // Interfaces: only user-declared ones describe plain data reliably + // (package and lib interfaces often describe class instances). + if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null; + } + if ( + checker.getCallSignatures(type).length > 0 || + checker.getConstructSignatures(type).length > 0 + ) + return null; + const memo = this.inProgress.get(type.id); + if (memo !== undefined) { + memo.name ??= `${DECL_BOUNDARY_PREFIX}r${type.id}`; + return memo.name; + } + const frame: { name: string | null } = { name: null }; + this.inProgress.set(type.id, frame); + try { + const members: string[] = []; + let readonlyCount = 0; + const props = checker.getPropertiesOfType(type); + for (const prop of props) { + const key = String(prop.escapedName); + if (key.startsWith("__@")) continue; // symbol-keyed (brands) + if ( + prop.flags & + (ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor) + ) + return null; + const propType = checker.getTypeOfSymbol(prop); + const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0; + let p = this.print(propType, depth + 1); + if (p === null) return null; + const readonly = (prop.checkFlags & READONLY_CHECK_FLAG) !== 0 || hasReadonlyModifier(prop); + if (readonly) readonlyCount++; + const name = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key); + if (optional && !/\bundefined\b/.test(p)) p = `${p} | undefined`; + members.push(`${readonly ? "readonly " : ""}${name}${optional ? "?" : ""}: ${p}`); + } + for (const info of checker.getIndexInfosOfType(type)) { + if (!(info.keyType.flags & ts.TypeFlags.String)) return null; + const v = this.print(info.type, depth + 1); + if (v === null) return null; + members.push(`[key: string]: ${v}`); + } + if (members.length === 0) return null; // `{}` admits any non-nullish value + // All-readonly object types may describe frozen values (.readonly()). + if (props.length > 0 && readonlyCount === props.length) return null; + const text = `{ ${members.join("; ")} }`; + if (frame.name !== null) { + this.declarations.push(`type ${frame.name} = ${text};`); + return frame.name; + } + return text; + } finally { + this.inProgress.delete(type.id); + } + } +} + +function hasReadonlyModifier(prop: ts.Symbol): boolean { + return prop.declarations.some( + (d) => + (ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) && + (d.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword), + ); +} + +/** Children of a node in source order (forEachChild order). */ +function childrenOf(node: ts.Node): ts.Node[] { + const out: ts.Node[] = []; + ts.forEachChild( + node, + (c) => { + out.push(c); + return undefined; + }, + (list) => { + for (const c of list.nodes) out.push(c); + return undefined; + }, + ); + return out; +} + +function isWriteTarget(node: ts.Node): boolean { + let child = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isBinaryExpression(parent) && parent.left === child) { + const k = parent.operatorToken.kind; + return k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment; + } + if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) { + const op = parent.operator; + return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken; + } + if (ts.isDeleteExpression(parent)) return true; + if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) return parent.initializer === child; + if (ts.isShorthandPropertyAssignment(parent)) return true; + if ( + ts.isArrayLiteralExpression(parent) || + ts.isPropertyAssignment(parent) || + ts.isSpreadElement(parent) + ) { + // Destructuring assignment targets: conservative, skip. + let up: ts.Node | undefined = parent; + while (up !== undefined && (ts.isArrayLiteralExpression(up) || ts.isObjectLiteralExpression(up) || ts.isPropertyAssignment(up) || ts.isSpreadElement(up))) + up = up.parent; + if (up !== undefined && ts.isBinaryExpression(up) && up.operatorToken.kind === ts.SyntaxKind.EqualsToken) + return true; + } + return false; +} + +function inOptionalChain(node: ts.Node): boolean { + const parent = node.parent; + if (parent === undefined) return false; + if ( + (ts.isPropertyAccessExpression(parent) || + ts.isElementAccessExpression(parent) || + ts.isCallExpression(parent)) && + parent.expression === node && + (parent.questionDotToken !== undefined || (parent.flags & ts.NodeFlags.OptionalChain) !== 0) + ) + return true; + return ( + (ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + ts.isCallExpression(node)) && + (node.flags & ts.NodeFlags.OptionalChain) !== 0 + ); +} + +function isCallee(node: ts.Node): boolean { + const p = node.parent; + return ( + p !== undefined && + (ts.isCallExpression(p) || ts.isNewExpression(p) || ts.isTaggedTemplateExpression(p)) && + (ts.isTaggedTemplateExpression(p) ? p.tag === node : p.expression === node) + ); +} + +function inTypeContext(node: ts.Node): boolean { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isTypeNode(p) || ts.isTypeQueryNode(p)) return true; + if (ts.isExpressionWithTypeArguments(p) && p.parent && ts.isHeritageClause(p.parent)) + return true; + if (ts.isStatement(p)) return false; + } + return false; +} + +function unwrapTransparent(node: ts.Expression): ts.Expression { + let e = node; + while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e)) + e = e.expression; + return e; +} + +/** Plans the projection for one program pair. Answers null when nothing + * in the user files projects (the program stays as it is). */ +export function planDeclProjection(input: DeclProjectionInput): DeclProjection | null { + const jsChecker = input.jsProgram.getTypeChecker(); + const declChecker = input.declProgram.getTypeChecker(); + const printer = new TypePrinter(declChecker); + const plan: string[] = []; + const edits = new Map(); + + const typeAt = (checker: ts.TypeChecker, node: ts.Node): ts.Type | null => { + try { + return checker.getTypeAtLocation(node); + } catch { + return null; + } + }; + const isAnyish = (t: ts.Type | null): boolean => t !== null && (t.flags & ANYISH) !== 0; + const resolveAlias = (s: ts.Symbol | undefined): ts.Symbol | undefined => { + if (s === undefined || !(s.flags & ts.SymbolFlags.Alias)) return s; + try { + return declChecker.getAliasedSymbol(s); + } catch { + return s; + } + }; + const declSymbol = (n: ts.Node): ts.Symbol | undefined => { + try { + return resolveAlias(declChecker.getSymbolAtLocation(n)); + } catch { + return undefined; + } + }; + const namesPackageSymbol = (node: ts.Node): boolean => { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + if (ts.isIdentifier(n) && declaredInPackage(declSymbol(n))) { + found = true; + return; + } + for (const c of childrenOf(n)) visit(c); + }; + visit(node); + return found; + }; + + for (const file of input.userFiles) { + const jsSf = input.jsProgram.getSourceFile(file); + const declSf = input.declProgram.getSourceFile(file); + if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue; + const fileEdits: Edit[] = []; + /** Candidate expressions (by js node) with their alias. */ + const exprCandidates = new Map(); + const exprOrder: ts.Node[] = []; + + const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => { + if ( + !ts.isTypeReferenceNode(jsNode) && + !ts.isIndexedAccessTypeNode(jsNode) && + !ts.isExpressionWithTypeArguments(jsNode) + ) + return false; + if (ts.isExpressionWithTypeArguments(jsNode)) { + if (!jsNode.parent || !ts.isHeritageClause(jsNode.parent)) return false; + if (!jsNode.parent.parent || !ts.isInterfaceDeclaration(jsNode.parent.parent)) return false; + } + let jsType: ts.Type | null; + let declType: ts.Type | null; + try { + jsType = ts.isExpressionWithTypeArguments(jsNode) + ? jsChecker.getTypeAtLocation(jsNode) + : jsChecker.getTypeFromTypeNode(jsNode as ts.TypeNode); + declType = ts.isExpressionWithTypeArguments(declNode) + ? declChecker.getTypeAtLocation(declNode) + : declChecker.getTypeFromTypeNode(declNode as ts.TypeNode); + } catch { + return false; + } + if (!isAnyish(jsType) || declType === null) return false; + if (!namesPackageSymbol(declNode)) return false; + const alias = printer.project(declType); + if (alias === null) return false; + const start = ts.tokenStartOfNode(jsNode); + fileEdits.push({ start, end: jsNode.end, text: alias }); + plan.push(`${file}:${start}: type ${jsSf.text.slice(start, jsNode.end)} -> ${alias}`); + return true; + }; + + const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => { + if (!ts.isExpression(jsNode)) return; + const isCall = + ts.isCallExpression(jsNode) || ts.isNewExpression(jsNode) || ts.isTaggedTemplateExpression(jsNode); + const isAccess = ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); + const isAwait = ts.isAwaitExpression(jsNode); + if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return; + if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return; + if (inOptionalChain(jsNode)) return; + const parent = jsNode.parent; + if (parent !== undefined && ts.isExpressionStatement(parent)) return; + if (parent !== undefined && (ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent))) + return; + if (ts.isIdentifier(jsNode)) { + if ( + parent !== undefined && + (ts.isPropertyAccessExpression(parent) && parent.name === jsNode) + ) + return; + } + const jsType = typeAt(jsChecker, jsNode); + if (!isAnyish(jsType)) return; + // Package-produced? + const d = declNode as ts.Expression; + let produced = false; + if (ts.isCallExpression(d) || ts.isNewExpression(d)) { + const callee = d.expression; + const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee; + produced = declaredInPackage(declSymbol(target)); + if (!produced) { + try { + const sig = declChecker.getResolvedSignature(d); + const decl = sig?.declaration; + produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)); + } catch { + produced = false; + } + } + } else if (ts.isTaggedTemplateExpression(d)) { + produced = declaredInPackage(declSymbol(d.tag)); + } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { + const recvDecl = typeAt(declChecker, d.expression); + const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression); + const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null; + produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs)); + } else if (ts.isAwaitExpression(d)) { + const inner = unwrapTransparent(d.expression); + produced = + (ts.isCallExpression(inner) || ts.isPropertyAccessExpression(inner)) && + (declaredInPackage( + declSymbol( + ts.isCallExpression(inner) + ? ts.isPropertyAccessExpression(inner.expression) + ? inner.expression.name + : inner.expression + : inner.name, + ), + ) || + isAnyish(typeAt(jsChecker, inner))); + } else if (ts.isIdentifier(d)) { + const raw = (() => { + try { + return declChecker.getSymbolAtLocation(d); + } catch { + return undefined; + } + })(); + produced = + raw !== undefined && + (raw.flags & ts.SymbolFlags.Alias) !== 0 && + declaredInPackage(resolveAlias(raw)); + } + if (!produced) return; + // A consumer that takes anything needs no conversion. + try { + const ctx = declChecker.getContextualType(d); + if (ctx !== undefined && ctx.flags & ANYISH) return; + } catch { + // no contextual type + } + const declType = typeAt(declChecker, d); + if (declType === null) return; + const alias = printer.project(declType); + if (alias === null) return; + exprCandidates.set(jsNode, alias); + exprOrder.push(jsNode); + }; + + const annotated = new Set(); + const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => { + if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) return; + const fn = jsNode.parent; + if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return; + if (jsNode.dotDotDotToken !== undefined) return; + const jsType = typeAt(jsChecker, jsNode.name); + if (!isAnyish(jsType)) return; + const declType = typeAt(declChecker, (declNode as ts.ParameterDeclaration).name); + if (declType === null) return; + // Only parameters the package's declarations type (a callback the + // package calls), not ones the user typed elsewhere. + try { + const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression); + if (ctxFn === undefined) return; + const sigs = declChecker.getCallSignatures(ctxFn); + if (!sigs.some((s) => s.declaration !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)))) + return; + } catch { + return; + } + const alias = printer.project(declType); + if (alias === null) return; + annotated.add(jsNode); + const name = jsNode.name; + const nameStart = ts.tokenStartOfNode(name); + const text = jsSf.text; + const bareArrow = + ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && text[ts.tokenStartOfNode(fn)] !== "(" && text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === "" ; + const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; + if (bareArrow) { + fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); + fileEdits.push({ start: afterName, end: afterName, text: `: ${alias})` }); + } else { + fileEdits.push({ start: afterName, end: afterName, text: `: ${alias}` }); + } + plan.push(`${file}:${nameStart}: parameter ${name.text}: ${alias}`); + }; + + const walk = (jsNode: ts.Node, declNode: ts.Node): void => { + if (jsNode.kind !== declNode.kind) return; + if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) { + if (considerType(jsNode, declNode)) return; + } else { + considerParam(jsNode, declNode); + considerExpr(jsNode, declNode); + } + const a = childrenOf(jsNode); + const b = childrenOf(declNode); + if (a.length !== b.length) return; + for (let i = 0; i < a.length; i++) walk(a[i]!, b[i]!); + }; + walk(jsSf, declSf); + + // Suppress candidates whose receiver chain another candidate types. + const symbolTyped = new Map(); + const willBeTyped = (e: ts.Node, depth = 0): boolean => { + if (depth > 32) return false; + if (exprCandidates.has(e)) return true; + if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e)) + return willBeTyped(e.expression, depth + 1); + if (ts.isAsExpression(e)) return false; + if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) + return willBeTyped(e.expression, depth + 1); + if (ts.isIdentifier(e)) { + let sym: ts.Symbol | undefined; + try { + sym = jsChecker.getSymbolAtLocation(e); + } catch { + return false; + } + if (sym === undefined) return false; + const known = symbolTyped.get(sym); + if (known !== undefined) return known; + symbolTyped.set(sym, false); + let result = false; + const decls = sym.declarations; + if (decls.length === 1) { + const decl = decls[0]!; + if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined && decl.type === undefined) + result = willBeTyped(decl.initializer, depth + 1); + else if (ts.isParameter(decl) && annotated.has(decl)) result = true; + else if (ts.isBindingElement(decl)) result = false; + } + symbolTyped.set(sym, result); + return result; + } + return false; + }; + const receiverOf = (e: ts.Node): ts.Node | null => { + if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression; + if (ts.isIdentifier(e)) return null; + return null; + }; + const kept: ts.Node[] = []; + for (const e of exprOrder) { + const recv = receiverOf(e); + if (recv !== null && willBeTyped(recv)) continue; + // An identifier candidate that is a user binding never gets here + // (only package imports are candidates). + kept.push(e); + } + // Nested candidates: keep only the innermost (an outer one is typed by + // the inner wrap). + const keptSet = new Set(kept); + const containsKept = (outer: ts.Node): boolean => { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + for (const c of childrenOf(n)) { + if (keptSet.has(c)) { + found = true; + return; + } + visit(c); + } + }; + visit(outer); + return found; + }; + for (const e of kept) { + if (!ts.isAwaitExpression(e) && containsKept(e) && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))) { + // `a.b.c` with `a.b` wrapped: the outer access reads typed data. + const recv = receiverOf(e); + if (recv !== null && (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv))) + continue; + } + const alias = exprCandidates.get(e)!; + const start = ts.tokenStartOfNode(e); + const stmt = e.parent; + let prefix = "("; + // A statement that would start with `(` after a line without a + // semicolon continues the previous expression: guard it. + let up: ts.Node | undefined = e; + while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; + if (up?.parent !== undefined && ts.isExpressionStatement(up.parent) && ts.tokenStartOfNode(up.parent) === start) + prefix = ";("; + void stmt; + fileEdits.push({ start, end: start, text: prefix }); + // Closing parts at one position close inner wraps first. + fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start }); + plan.push(`${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`); + } + + if (fileEdits.length === 0) continue; + edits.set(file, fileEdits); + } + + if (edits.size === 0) return null; + const files = new Map(); + for (const [file, list] of edits) { + const sf = input.jsProgram.getSourceFile(file)!; + files.set(file, applyEdits(sf.text, list)); + } + const declFile = `${input.outDir.split("\\").join("/")}/${DECL_BOUNDARY_FILE}`; + const declText = + "// Generated by scriptc: types projected from npm package declarations.\n" + + printer.declarations.join("\n") + + "\n"; + return { files, declFile, declText, plan }; +} + +/** Applies insertions and replacements; overlapping replacements keep the + * first. Insertions at one position keep their order. */ +export function applyEdits(text: string, edits: readonly Edit[]): string { + const sorted = edits + .map((e, i) => ({ ...e, i })) + .sort( + (a, b) => a.start - b.start || a.end - b.end || (a.rank ?? 0) - (b.rank ?? 0) || a.i - b.i, + ); + let out = ""; + let at = 0; + for (const e of sorted) { + if (e.start < at) continue; // overlaps an earlier replacement + out += text.slice(at, e.start) + e.text; + at = e.end; + } + return out + text.slice(at); +} diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index c3b4ad906..58992da54 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -288,6 +288,10 @@ export function applyWidthLift( case "dynIn": { if (dst.kind !== "dyn") throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot"); + // The declaration-typed boundary keeps user values' identity when + // they cross into package code (decl-projection.ts). + if (lowerer.declBoundary && !/[\\/]node_modules[\\/]/.test(loc.file) && liveBoxable(lowerer, value.type)) + return { kind: "dynFrom", value, liveRef: true, type: DYN, loc }; return { kind: "dynFrom", value, type: DYN, loc }; } case "dynOut": @@ -775,3 +779,16 @@ function buildRecordProjection( loc, }; } + +/** Records and arrays (and unions holding them), whose identity a live + * capsule keeps. */ +function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set()): boolean { + if (t.kind === "record") { + const shape = lowerer.shapes.get(t.shapeId); + return shape !== undefined && !shape.tuple; + } + if (t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index acac99610..d0064e56c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -13,6 +13,7 @@ import { STRING, type SrcLoc, UNDEFINED_T, + VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, @@ -47,6 +48,7 @@ import { import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js"; import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js"; import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js"; +import { isDeclBoundaryTypeNode } from "../../decl-projection.js"; /** `c ? a : b`. An explicit destination constructs each branch at that * layout before joining it. Without one, retain the expression's inferred @@ -1516,6 +1518,12 @@ export function lowerAsExpression( const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs); if (!target) lowerer.badType(expr.type, targetTs); if (target.kind === "dyn") return inner; // `as unknown`: erasure + // The declaration-typed boundary (decl-projection.ts): an identity- + // preserving checked conversion of a value package code produced. + if (isDeclBoundaryTypeNode(expr.type)) { + const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr)); + if (adopted) return adopted; + } // An all-`unknown`-fields record target (`err as { code?: unknown }` // — the errno-probing idiom): there is nothing to validate (every // field is unknown, exactly what the dyn value already answers) and @@ -1603,3 +1611,69 @@ export function lowerAsExpression( } return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) }; } + +/** True when a type holds a record or an array somewhere (values whose + * identity can be observed). */ +function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): boolean { + if (t.kind === "record" || t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false; +} + +/** `( as __scriptc_decl_N)`: check the dynamic value against + * the projected type and build the native value (dynCheck), then adopt: + * every source object or array that something else still references + * becomes a live capsule of the value built from it + * (scr_dyn_decl_adopt), so identity and mutation visibility hold on both + * sides. Scalar targets are ordinary checked casts. */ +function declBoundaryCast( + lowerer: Lowerer, + inner: IrExpr, + target: IrType, + loc: SrcLoc, +): IrExpr | null { + if ( + !canDynCheckTo( + target, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) && + !(target.kind === "union" && lowerer.dynConvertible(target)) + ) + return null; + if (!holdsReference(lowerer, target)) { + return { kind: "dynCheck", value: inner, type: target, loc }; + } + const source = lowerer.declareHiddenLocal("%boundarySource", DYN); + const value = lowerer.declareHiddenLocal("%boundaryValue", target); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: source.id, init: inner, loc }, + { + kind: "varDecl", + localId: value.id, + init: { kind: "dynCheck", value: varRef(source.id, DYN, loc), adopt: true, type: target, loc }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.declAdopt", + args: [ + varRef(source.id, DYN, loc), + { kind: "dynFrom", value: varRef(value.id, target, loc), liveRef: true, type: DYN, loc }, + ], + type: VOID, + loc, + }, + loc, + }, + ], + result: varRef(value.id, target, loc), + type: target, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index 4adfb87ab..df4e747d6 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -227,6 +227,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const opaqueSeen = new Set(); const runtimeShapes = new Set(); const runtimeSeen = new Set(); + /** Shapes the declaration-typed boundary converts into (adopting casts): + * their values come from package code that builds them in the + * declaration's order, the order their table holds. */ + const declaredShapes = new Set(); + const declaredSeen = new Set(); const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); else shapesIn(type, runtimeShapes, runtimeSeen); @@ -332,7 +337,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio producedBy(e.type, true); return; case "dynCheck": - if (!orderPreservingClone(e)) producedBy(e.type, false); + if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); + else if (!orderPreservingClone(e)) producedBy(e.type, false); return; case "dynDestrCheck": case "caughtCheck": @@ -392,6 +398,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio f.opaque = true; } for (const id of runtimeShapes) factsOf(id).runtime = true; + for (const id of declaredShapes) if (!runtimeShapes.has(id)) factsOf(id).pinned = true; // ── scope ─────────────────────────────────────────────────────────── // Only plain declared-field shapes with a table take part. diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..c1b9a0ff8 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,3 +1,4 @@ +import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; @@ -3565,6 +3566,18 @@ export class Lowerer { else if (kind === "failed") s.failed += 1; } + /** The program holds the declaration-typed boundary's projection + * (decl-projection.ts): user-file record and array values crossing into + * dynamic slots (package parameters) box as live capsules, so a value + * that comes back through a boundary cast keeps its identity. */ + private declBoundaryMemo: boolean | null = null; + get declBoundary(): boolean { + this.declBoundaryMemo ??= this.program + .getSourceFiles() + .some((sf) => isDeclBoundaryFile(sf.fileName)); + return this.declBoundaryMemo; + } + constructor( readonly program: ts.Program, readonly entry: ts.SourceFile, @@ -3687,8 +3700,11 @@ export class Lowerer { classImplementedProtocol: (type) => this.classImplementedProtocol(type), isStdlibFile: this.isStdlibFile, isNpmFile: this.isNpmFile, + // The declaration-typed boundary's generated types (decl-projection.ts) + // describe data, like an explicitly mapped external type surface. isExternalTypeFile: (sf) => - this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))), + this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))) || + isDeclBoundaryFile(sf.fileName), typeMemo: this.typeMemo, ...(this.generalNodeStreams ? { generalNodeStreams: true } : {}), ...(this.dynNodeStreams ? { dynNodeStreams: true } : {}), @@ -7708,6 +7724,7 @@ export class Lowerer { entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), + ...(this.declBoundary ? { declBoundary: true as const } : {}), ...(this.threadSlotKeys.size > 0 ? { threadSlotKeys: [...this.threadSlotKeys].sort() } : {}), diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 507d3392d..ef5e46d50 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -139,6 +139,7 @@ import { typeOnlyReachedFiles, } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; +import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -397,6 +398,9 @@ export interface LoadResult { /** The program's pre-emit diagnostics, computed once. */ diagnostics: readonly ts.Diagnostic[]; projectWorld: ts.Program | null; + /** The declaration-typed boundary rewrote user files: errors fall + * back to the declaration world (load.projectWorld()), as without it. */ + declWorldFallback?: true; }; /** How scriptc's type world differs from the project's own tsc setup — * used only to label the TypeScript errors those differences cause. */ @@ -846,6 +850,9 @@ function loadProgram7( externalTypes: ReadonlyMap = new Map(), /** The --npm-static lowering world: root opted-in packages' deep edges. */ rootNpmStaticEdges = false, + /** The declaration-typed boundary's projection (decl-projection.ts): + * answers rewritten user files for the lowering world, or null. */ + projector?: (program: ts.Program) => DeclProjection | null, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; @@ -940,6 +947,53 @@ function loadProgram7( }; for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text); program = host.createProgram([...programRoots, overridesDtsPath()], options); + } else if (projector !== undefined) { + // The declaration-typed boundary: user files typed from package + // declarations (decl-projection.ts). The tsc gate reads the original + // text; the lowering compiles the rewritten files. + const projection = projector(program); + if (projection !== null) { + const original = program; + const baseline = new Map(); + for (const file of projection.files.keys()) { + const sf = original.getSourceFile(file); + if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length); + } + for (const [file, text] of projection.files) host.addVirtualFile(file, text); + host.addVirtualFile(projection.declFile, projection.declText); + programRoots.push(projection.declFile); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + // A rewrite the lowering world disagrees with (new errors) is + // dropped for that file: the file lowers as it would without it. + const dropped: string[] = []; + for (const file of projection.files.keys()) { + const sf = program.getSourceFile(file); + if (sf === undefined) continue; + if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) { + dropped.push(file); + host.addVirtualFile(file, original.getSourceFile(file)!.text); + } + } + if (dropped.length > 0) { + program.dispose(); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + for (const file of dropped) { + config.diags.push(declBoundaryDroppedDiag(file)); + } + } + if (dropped.length === projection.files.size) { + program.dispose(); + programRoots.pop(); + program = original; + } else { + tscWorld = { + program: original, + diagnostics: ts.getPreEmitDiagnostics(original), + projectWorld: null, + declWorldFallback: true, + }; + } + } } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); @@ -1156,31 +1210,69 @@ export function loadProgram( (npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null, }; const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow }); + let authoredHost: ts.Ts7Host | null = null; + let authoredLoad: ReturnType | null = null; + const authored = (): ReturnType => { + if (!authoredLoad) { + authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); + authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); + } + return authoredLoad; + }; + const declPackages = npmStaticPackages.filter((pkg) => { + const resolved = resolveBareModule(entryPath, pkg, "types-only"); + return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile); + }); + const projector = + declPackages.length === 0 + ? undefined + : (program: ts.Program): DeclProjection | null => { + const userFiles = declBoundaryUserFiles(program, declPackages); + if (userFiles.length === 0) return null; + let projection: DeclProjection | null; + try { + projection = planDeclProjection({ + jsProgram: program, + declProgram: authored().program, + userFiles, + outDir: dirname(entryPath), + }); + } catch (error) { + if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error; + return null; + } + if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") { + process.stderr.write( + `[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`, + ); + if (projection !== null) process.stderr.write(projection.declText); + } + return projection; + }; try { - const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0); - let authoredHost: ts.Ts7Host | null = null; - let authoredLoad: ReturnType | null = null; + const load = loadProgram7( + host, + entryPath, + externalTypes, + npmStaticPackages.length > 0, + projector, + ); return { ...load, services, projectWorld: npmStaticPackages.length === 0 ? load.projectWorld - : () => { + : () => // Check authored code against the package's published declarations. // Runtime bodies still lower from JavaScript in the original world; // inference there must not manufacture errors in valid user code. - if (!authoredLoad) { - authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); - authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); - } - return authoredLoad.projectWorld(); - }, + authored().projectWorld(), dispose: () => { try { - authoredLoad?.disposeAll(); + (authoredLoad as ReturnType | null)?.disposeAll(); } finally { - authoredHost?.close(); + (authoredHost as ts.Ts7Host | null)?.close(); try { load.disposeAll(); } finally { @@ -1190,11 +1282,39 @@ export function loadProgram( }, }; } catch (error) { + (authoredLoad as ReturnType | null)?.disposeAll(); + (authoredHost as ts.Ts7Host | null)?.close(); host.close(); throw error; } } +/** User TypeScript files (outside node_modules) that import one of the + * packages whose declarations the boundary projects from. */ +function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] { + const out: string[] = []; + for (const sf of program.getSourceFiles()) { + const name = sf.fileName.split("\\").join("/"); + if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) + continue; + if (name.endsWith(".tsx")) continue; // JSX files: not projected yet + const text = sf.text; + if (packages.some((pkg) => text.includes(`"${pkg}"`) || text.includes(`'${pkg}'`) || text.includes(`"${pkg}/`) || text.includes(`'${pkg}/`))) + out.push(sf.fileName); + } + return out; +} + +/** SC6102 — a projected file the lowering world disagreed with. */ +function declBoundaryDroppedDiag(file: string): ScrDiagnostic { + return { + code: "SC6102", + message: + "values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript", + loc: { file, start: 0, end: 0 }, + }; +} + /** tsc diagnostics (syntax + types), the supported-import fence, and the * module evaluation order: fills load.moduleOrder (the SourceFiles * themselves — the lowering consumes them directly) and returns the @@ -4801,7 +4921,9 @@ function preflight7(load: LoadResult): { tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics); if (tscErrors.length > 0) { const world = - tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program); + tscWorld === undefined || tscWorld.declWorldFallback === true + ? load.projectWorld() + : (tscWorld.projectWorld ?? tscWorld.program); for (const d of errorsOf(world)) { const diag = toPassthrough(d); const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 3dc6dcebd..2d5e87ffb 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -610,6 +610,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // fence; the zero-argument form always throws ERR_MISSING_ARGS. The // DOMException clone shares the option validation. "dyn.structuredClone", + "dyn.declAdopt", "dyn.cloneMissing", "dyn.cloneTransferFail", "dyn.cloneTransferFailWith", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..d4b2cf2d2 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3776,6 +3776,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's * own (verbatim, doubly-wrapped) message. */ "dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN }, + /** The declaration-typed boundary's adoption (decl-projection.ts): + * `source` is the dynamic value a boundary cast just converted, and + * `capsule` a live capsule of the native value built from it. Every + * object or array of `source` that something else still references + * becomes, in place, a live capsule of the native value built from it. + * Throws the path TypeError, changing nothing, when a converted object + * is not plain data (a prototype, accessors, symbols, frozen state). */ + "dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID }, "dyn.cloneMissing": { argTypes: [], result: DYN }, /** structuredClone with a NON-EMPTY transfer array of static values: * nothing static is transferable, so the call always throws Node's diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 12400fee9..067f79a26 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1044,6 +1044,10 @@ export interface IrModule { entry: string; /** Executable roots run in isolated native worker contexts. */ workers?: boolean; + /** The program has declaration-typed boundary casts (decl-projection.ts): + * closures boxed for package code return records and arrays as live + * capsules, so a value that comes back keeps its identity. */ + declBoundary?: true; /** The program constructs LifetimeRegion (@scriptc/experimental): every * ProgramLifetime instance lives in a counted region (the region.* * intrinsics; backend/llvm/regions.ts). */ @@ -3879,10 +3883,19 @@ export type IrExpr = /** `exact`: the value is known to be a capsule this program boxed from * exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the * expression), so the check is one load of the capsule's instance. */ + /** `adopt`: the declaration-typed boundary's cast (decl-projection.ts). + * The conversion preserves identity: a source object or array that + * something else still references (reference count above one) is + * turned, after the whole check succeeded, into a live capsule of the + * value built from it, so every holder sees the same native value; an + * unshared source is simply consumed. Values must be plain data + * (ordinary objects and arrays without accessors, prototypes, symbols + * or frozen state), otherwise the cast throws the path TypeError. */ | { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; + adopt?: true; exact?: true; type: IrType; loc: SrcLoc; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 63aa8ff5c..adab9628f 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -120,6 +120,8 @@ export function validateModule(mod: IrModule): IrValidationError[] { const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 }; if (mod.workers !== undefined && typeof mod.workers !== "boolean") errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc }); + if (mod.declBoundary !== undefined && mod.declBoundary !== true) + errors.push({ message: "declBoundary must be true when present", loc: moduleLoc }); if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true) errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc }); if (mod.workers && mod.lib !== undefined) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..987de7758 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1000,6 +1000,11 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); * retained view released with the record. Its capsules adopt that view, and * the typed serializers consult `has` to serialize through the capsule. * `apply` consumes a fresh copying snapshot and returns it merged (+1). */ +/* The declaration-typed boundary's adoption (scr_json.c): every object or + * array of `source` that something else still references becomes a live + * capsule of the native value `capsule` built from it. Throws the path + * TypeError, changing nothing, when a converted node is not plain data. */ +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names, const size_t *lens, size_t count); bool scr_record_source_has(const void *rec); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..336d42e90 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15755,3 +15755,210 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) { return true; return scr_dyn_object_proto_name(key); } + +/* ── declaration-typed boundary: adoption ─────────────────────────────── + * A boundary cast (decl-projection.ts in the compiler) checks a value + * package code produced against the type the package's declarations give + * it and builds the native value. Copying would be observable whenever + * something else still references a source object or array, so this pass + * walks the source next to a live capsule of the built value and turns + * every source node with another holder (rc above one) into a live + * capsule of the native value built from it: every holder then reads and + * writes the same native value. Unshared nodes are left to be released + * with their parent, which no program can tell apart from a move. + * + * Phase one checks that every converted node is plain data (the native + * value has no prototype, accessors, symbol keys or frozen state) and + * collects the shared ones; it throws the path TypeError before anything + * changes. Phase two converts the shared nodes in place. Programs without + * boundary casts never reference this code (the linker drops it). */ +typedef struct { + ScrDyn *node; + ScrDyn *capsule; /* owned */ +} ScrDeclAdopt; + +typedef struct { + ScrDeclAdopt *items; + size_t len, cap; +} ScrDeclAdoptList; + +static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { + if (list->len == list->cap) { + list->cap = list->cap ? list->cap * 2 : 8; + list->items = realloc(list->items, list->cap * sizeof *list->items); + if (!list->items) scr_trap("scriptc: out of memory\n"); + } + list->items[list->len].node = node; + list->items[list->len].capsule = capsule; + list->len++; +} + +static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { + const char *want = NULL; + if (n->kind == SCR_DYN_OBJ) { + if (n->prototype || n->null_proto || n->v.obj.class_name) + want = "a plain object (this value has a prototype)"; + else if (n->symbol_properties || n->symbol_keys) + want = "a plain object (this value has symbol-keyed properties)"; + else if (n->non_extensible) + want = "a plain object (this value is frozen, sealed or not extensible)"; + else if (n->v.obj.source_access && !n->v.obj.source_identity) + want = "a plain object"; + else { + for (size_t i = 0; i < n->v.obj.len && !want; i++) { + const ScrDynEntry *e = &n->v.obj.entries[i]; + if (e->accessor) want = "a plain object (this value has accessor properties)"; + else if (!e->writable || !e->enumerable || !e->configurable) + want = "a plain object (this value has non-default property attributes)"; + } + } + } else if (n->kind == SCR_DYN_ARR) { + if (n->prototype) + want = "a plain array (this value has a prototype)"; + else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) + want = "a plain array (this value has extra properties)"; + else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) + want = "a plain array (this value is frozen, sealed or not extensible)"; + } + if (!want) return true; + scr_dyn_check_fail(path, want, n); + return false; +} + +static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { + for (size_t i = 0; i < list->len; i++) + if (list->items[i].node == node) return list->items[i].capsule; + return NULL; +} + +/* `capsule` is owned by the call: pushed or released. */ +static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, + const ScrDynPath *path, unsigned depth) { + if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || + capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { + scr_dyn_release(capsule); + return true; + } + if (!scr_decl_adopt_plain(n, path)) { + scr_dyn_release(capsule); + return false; + } + if (n->rc > 1 && n->rc != SIZE_MAX) { + ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); + if (seen) { + bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; + scr_dyn_release(capsule); + if (!same) + scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); + return same; + } + scr_decl_adopt_push(list, n, capsule); + } + void *ptr = capsule->v.typed_ref.ptr; + if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; + for (size_t i = 0; i < ops->count; i++) { + if (!ops->has(ptr, i)) continue; + const ScrStr *name = ops->names[i]; + ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, name->data, 0 }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && + capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; + for (size_t i = 0; i < n->v.arr.len; i++) { + ScrDyn *child = n->v.arr.items[i]; + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return true; +} + +/* Weak collections key a plain object by its node and a capsule by its + * native identity: move the node's entries to the identity it now has. */ +static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) { + bool moved = false; + if (!scr_weak_observing) return false; + for (;;) { + ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)]; + while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next; + ScrWeakEntry *entry = *slot; + if (!entry) return moved; + *slot = entry->all_next; + entry->key = ptr; + entry->kind = 32; + ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)]; + entry->all_next = *head; + *head = entry; + moved = true; + } +} + +static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) { + ScrDynRegion *region = scr_dyn_region_of(n); + if (region) region->intact = false; + /* The old payload moves to a fresh node, which is released below. A + * misc node carries no buffers of an earlier life. */ + ScrDyn *old = scr_dyn_alloc(SCR_DYN_NULL); + old->kind = n->kind; + old->buffer = n->buffer; + old->null_proto = n->null_proto; + old->copied_from_native = n->copied_from_native; + old->non_extensible = n->non_extensible; + old->v = n->v; + old->prototype = n->prototype; + old->symbol_properties = n->symbol_properties; + old->symbol_keys = n->symbol_keys; + n->kind = SCR_DYN_TYPED_REF; + n->buffer = false; + n->null_proto = false; + n->copied_from_native = false; + n->non_extensible = false; + n->prototype = NULL; + n->symbol_properties = NULL; + n->symbol_keys = NULL; + memset(&n->v, 0, sizeof n->v); + n->v.typed_ref.ptr = capsule->v.typed_ref.retain(capsule->v.typed_ref.ptr); + n->v.typed_ref.retain = capsule->v.typed_ref.retain; + n->v.typed_ref.release = capsule->v.typed_ref.release; + n->v.typed_ref.type_key = capsule->v.typed_ref.type_key; + n->v.typed_ref.type_key_len = capsule->v.typed_ref.type_key_len; + n->v.typed_ref.materialize = capsule->v.typed_ref.materialize; + n->v.typed_ref.commit = capsule->v.typed_ref.commit; + n->v.typed_ref.array = capsule->v.typed_ref.array; + n->v.typed_ref.record = capsule->v.typed_ref.record; + n->v.typed_ref.klass = capsule->v.typed_ref.klass; + n->v.typed_ref.traced = capsule->v.typed_ref.traced; + n->v.typed_ref.observed = capsule->v.typed_ref.observed; + if (scr_decl_adopt_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; + /* A record with undeclared properties answers them from its retained + * view, as a freshly boxed capsule does. */ + if (scr_record_source_count && n->v.typed_ref.type_key_len >= 7 && + memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { + ScrDyn *view = scr_record_source_view(n->v.typed_ref.ptr); + if (view) n->v.typed_ref.materialized = scr_dyn_retain(view); + } + scr_dyn_release(old); +} + +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { + ScrDeclAdoptList list = { NULL, 0, 0 }; + bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); + for (size_t i = 0; i < list.len; i++) { + ScrDeclAdopt *item = &list.items[i]; + if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_decl_adopt_become(item->node, item->capsule); + scr_dyn_release(item->capsule); + } + free(list.items); +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/main.ts b/tests/fixtures/npm/cases/zod3-boundary/main.ts new file mode 100644 index 000000000..8e8cedee7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/main.ts @@ -0,0 +1,83 @@ +// Values that cross from zod 3.25 (compiled from its JavaScript) into code +// typed by zod's declarations: identity, aliasing and mutation after parse, +// values zod hands back unchanged, callbacks zod calls, typed array work, +// and failing checks. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Typed records: field reads, writes and array work on parsed data. +const Item = z.object({ + sku: z.string(), + price: z.number().nonnegative(), + qty: z.number().int(), + tags: z.array(z.string()).default([]), +}); +type Item = z.infer; +const items: Item[] = z.array(Item).parse([ + { sku: "a", price: 2.5, qty: 4, tags: ["x"] }, + { sku: "b", price: 10, qty: 1 }, + { sku: "c", price: 0.5, qty: 10, tags: ["x", "y"] }, +]); +const total = items.reduce((sum, it) => sum + it.price * it.qty, 0); +console.log("total", total); +const byTag = new Map(); +for (const it of items) for (const t of it.tags) byTag.set(t, (byTag.get(t) ?? 0) + it.qty); +console.log("byTag", [...byTag].map(([k, v]) => `${k}=${v}`).join(",")); +const sorted = [...items].sort((a, b) => b.price - a.price).map((it) => it.sku); +console.log("sorted", sorted.join(",")); + +// Mutation after parse is visible through every reference. +const first = items[0]!; +first.qty += 1; +first.tags.push("z"); +console.log("mutated", items[0]!.qty, items[0]!.tags.join("|"), first === items[0]); +show("item", items[0]); + +// A value zod hands back unchanged keeps its identity and its mutations. +const Point = z.custom<{ x: number; y: number }>(); +const original = { x: 1, y: 2 }; +const same = Point.parse(original); +same.x = 10; +console.log("custom", original.x, same === original); +original.y = 20; +console.log("custom back", same.y); + +// A shared object returned by a transform: every parse sees the same one. +const shared = { hits: 0 }; +const Counter = z.string().transform(() => shared); +const c1 = Counter.parse("a"); +const c2 = Counter.parse("b"); +c1.hits += 1; +console.log("shared", c2.hits, shared.hits, c1 === c2); + +// A refine callback reads typed fields of the value zod checks. +const Range = z + .object({ lo: z.number(), hi: z.number() }) + .refine((r) => r.lo <= r.hi, { message: "lo must not exceed hi" }); +console.log("range ok", JSON.stringify(Range.parse({ lo: 1, hi: 3 }))); +const bad = Range.safeParse({ lo: 5, hi: 3 }); +console.log("range bad", bad.success ? "?" : bad.error.issues[0]?.message); + +// safeParse data, a discriminated union, and passthrough keys. +const Event = z.discriminatedUnion("type", [ + z.object({ type: z.literal("click"), x: z.number(), y: z.number() }), + z.object({ type: z.literal("key"), key: z.string() }), +]); +type Event = z.infer; +const describe = (e: Event): string => (e.type === "click" ? `click@${e.x},${e.y}` : `key:${e.key}`); +const r = Event.safeParse({ type: "key", key: "Enter" }); +if (r.success) console.log("event", describe(r.data)); +const Loose = z.object({ id: z.number() }).passthrough(); +const loose = Loose.parse({ extra: true, id: 3, more: "m" }); +console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose)); + +// async parse +async function main(): Promise { + const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2 }); + console.log("async", it.sku, it.price * it.qty, it.tags.length); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + private _cached; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-boundary/package.json b/tests/fixtures/npm/cases/zod3-boundary/package.json new file mode 100644 index 000000000..be0bf47db --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/package.json @@ -0,0 +1 @@ +{ "name": "zod3-boundary-case", "private": true, "type": "module" } From 7f7e11a1a749beb0f14d340fdb193e99e013c18b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:21:31 -0700 Subject: [PATCH 04/30] Read and write Error.stackTraceLimit through the Error constructor value zod 4 saves and restores the limit through an alias, `const E = "captureStackTrace" in Error ? Error : null`. The interned Error constructor value now answers `stackTraceLimit` from the same limit the static member and stack capture use, and numeric writes set it, so `E.stackTraceLimit` reads 10 instead of undefined and `E.stackTraceLimit = 0` reaches `Error.stackTraceLimit`. Co-Authored-By: Claude Opus 5.5 --- .../7602-error-stack-limit-value.js.json | 6 ++++++ packages/runtime/src/scr_json.c | 13 +++++++++++++ tests/corpus/7602-error-stack-limit-value.js | 18 ++++++++++++++++++ .../7602-error-stack-limit-value.js.json | 1 + 4 files changed, 38 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json create mode 100644 tests/corpus/7602-error-stack-limit-value.js create mode 100644 tests/harness/corpus-costs/7602-error-stack-limit-value.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json new file mode 100644 index 000000000..99e1b8904 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7602-error-stack-limit-value.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..074fe3eae 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1870,6 +1870,14 @@ ScrDyn *scr_dyn_error_constructor(const char *name) { return NULL; } +/* `Error.stackTraceLimit` read or written through the Error constructor + * held as a value (zod's `const E = Error; E.stackTraceLimit = 0`): the + * same limit the static member reads and stack capture honors. */ +static bool scr_dyn_is_stack_limit(const ScrDyn *d, const char *key, size_t key_len) { + return key_len == 15 && memcmp(key, "stackTraceLimit", 15) == 0 && d->kind == SCR_DYN_FUNC && + scr_error_ctor_values[0] && d->v.fn.clo == scr_error_ctor_values[0]->v.fn.clo; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -8992,6 +9000,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; + if (value->kind == SCR_DYN_NUM && scr_dyn_is_stack_limit(recv, key->data, key->len)) { + scr_stack_limit_set(value->v.num); + return; + } ScrDyn *table = scr_dyn_fn_properties(recv); scr_dyn_object_key_set(table, key, value, recv); scr_dyn_release(table); @@ -11773,6 +11785,7 @@ static ScrDyn *scr_function_constructor(uint32_t kind) { /* Keyed read on a FUNC node (see scr_runtime.h): own props first, then * the function-instance built-ins name/length. +1 or NULL. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { + if (scr_dyn_is_stack_limit(d, key, key_len)) return scr_dyn_new_num(scr_stack_limit_get()); ScrClosure *owner = scr_closure_identity(d->v.fn.clo); if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props) owner->props = scr_box_retain(d->v.fn.class_obj->static_data); diff --git a/tests/corpus/7602-error-stack-limit-value.js b/tests/corpus/7602-error-stack-limit-value.js new file mode 100644 index 000000000..4b3f8cc7e --- /dev/null +++ b/tests/corpus/7602-error-stack-limit-value.js @@ -0,0 +1,18 @@ +// Error.stackTraceLimit through the Error constructor held as a value +// (zod 4's `const E = "captureStackTrace" in Error ? Error : null`): reads +// and writes reach the same limit as the static member. +const E = "captureStackTrace" in Error ? Error : null; +console.log(typeof E, E === Error, E.stackTraceLimit, Error.stackTraceLimit); +const saved = E.stackTraceLimit; +E.stackTraceLimit = 0; +console.log(Error.stackTraceLimit, E.stackTraceLimit, typeof saved === "number"); +console.log(new Error("no frames").stack); +try { + E.stackTraceLimit = 3; + console.log(Error.stackTraceLimit); +} finally { + E.stackTraceLimit = saved; +} +console.log(E.stackTraceLimit, Error.stackTraceLimit); +Error.stackTraceLimit = 4; +console.log(E.stackTraceLimit); diff --git a/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } From 3fff00e5f77027602ac913eced1cd9f1a55f0cf0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:21:31 -0700 Subject: [PATCH 05/30] Fold navigator member reads under optional chains zod 4 probes `navigator?.userAgent?.includes("Cloudflare")`. Node's navigator global is never nullish and has no value form here, so the optional step is the plain member read: `navigator?.userAgent` folds to the same `Node.js/` literal as `navigator.userAgent`, instead of lowering the navigator object as a value and refusing at run time. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/expressions/optional-chains.ts | 11 +++++++++++ .../frontend/lowering/expressions/property-access.ts | 8 ++++---- .../corpus/7600-navigator-optional-chain.js.json | 6 ++++++ tests/corpus/7600-navigator-optional-chain.js | 7 +++++++ .../7600-navigator-optional-chain.js.json | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json create mode 100644 tests/corpus/7600-navigator-optional-chain.js create mode 100644 tests/harness/corpus-costs/7600-navigator-optional-chain.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 4b8c5692a..104d85a80 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -1,3 +1,4 @@ +import { isNavigatorGlobal } from "../builtins/navigator.js"; import { isSafeToDiscard } from "./evaluation-safety.js"; import { completeFuncValueArgs } from "../call-arguments.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; @@ -223,6 +224,16 @@ export function lowerOptionalChain( // Native handle methods have no first-class value to guard; when the // method read itself has no lowering, re-dispatch the plain call (which // reports its own diagnostics) instead of failing on the value read. + // `navigator?.userAgent`: Node's navigator global is never nullish and + // has no value form, so the guarded step is the plain member read. + if (isNavigatorGlobal(lowerer, recvNode)) { + lowerer.chainHandled.add(dotNode); + try { + return lowerer.lowerExpr(expr); + } finally { + lowerer.chainHandled.delete(dotNode); + } + } let loweredReceiver: IrExpr | null; if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) { loweredReceiver = tryLowerExpression(lowerer, recvNode); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 2e6ab981a..498038831 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -306,10 +306,10 @@ export function lowerPropertyAccessExpr( }; } } - if (!expr.questionDotToken) { - const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); - if (navigator) return navigator; - } + // Node's navigator is never nullish, so `navigator?.userAgent` (zod's + // Cloudflare probe) folds like `navigator.userAgent`. + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; // A dgram socket's method read as a value in JavaScript (`emit: // socket.emit.bind(socket)`): the socket crosses as its checked-dynamic // handle, whose methods are function values that dispatch on their diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json new file mode 100644 index 000000000..12465d5e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7600-navigator-optional-chain.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7600-navigator-optional-chain.js" + ], + "diags": [] +} diff --git a/tests/corpus/7600-navigator-optional-chain.js b/tests/corpus/7600-navigator-optional-chain.js new file mode 100644 index 000000000..5c1dac442 --- /dev/null +++ b/tests/corpus/7600-navigator-optional-chain.js @@ -0,0 +1,7 @@ +// Node's navigator global read through optional chains (zod 4's +// `navigator?.userAgent?.includes("Cloudflare")` probe): navigator is +// never nullish, so each chain answers like the plain member read. +const cloudflare = typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare"); +console.log(cloudflare); +console.log(navigator?.userAgent === navigator.userAgent, navigator?.userAgent?.startsWith("Node.js/")); +console.log(navigator?.userAgent?.split("/")[0] ?? "none"); diff --git a/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json b/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json new file mode 100644 index 000000000..e350cf2da --- /dev/null +++ b/tests/harness/corpus-costs/7600-navigator-optional-chain.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 22000 } From 3041df387d772511f0e8dbb04ca9aa4d20dd81f3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 17:43:57 -0700 Subject: [PATCH 06/30] Spread untyped iterables into Math.min and Math.max Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-stdlib.ts | 19 ++++++++++ packages/compiler/src/ir/builtin-effects.ts | 2 ++ .../compiler/src/ir/builtin-signatures.ts | 1 + .../corpus/7488-math-spread-untyped.js.json | 6 ++++ packages/runtime/src/scr_dyn.h | 3 ++ packages/runtime/src/scr_json.c | 35 +++++++++++++++++++ tests/corpus/7488-math-spread-untyped.js | 33 +++++++++++++++++ .../7488-math-spread-untyped.js.json | 1 + 9 files changed, 101 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json create mode 100644 tests/corpus/7488-math-spread-untyped.js create mode 100644 tests/harness/corpus-costs/7488-math-spread-untyped.js.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..488b81d57 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -973,6 +973,7 @@ export const LIB_FN_SYMS: Record = { "dyn.typedRefIs": "scr_dyn_typed_ref_is_key", "dyn.classIs": "scr_dyn_class_is_key", "dyn.iterPack": "scr_dyn_iter_pack", + "dyn.mathFold": "scr_dyn_math_fold", "dyn.destructureRest": "scr_dyn_destructure_rest", "dyn.iterator": "scr_dyn_iterator", "dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open", diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 0beb24a0e..e671a0a08 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2496,6 +2496,25 @@ export function lowerStdlibMethodCall( // method, drains into the number[] the fold reads. src = classGeneratorIterable(lowerer, src) ?? src; if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src; + // An untyped iterable (`Math.min(...caps.map(f))` in JavaScript): the + // runtime packs its values and folds their ToNumber like Node. V8 names + // a null or undefined source by its text, calls as `f(...)`. + if (src.type.kind === "dyn") { + const what = ts.isCallExpression(spread.expression) + ? `${ts.getTextOfNode(spread.expression.expression)}(...)` + : ts.getTextOfNode(spread.expression); + return { + kind: "libCall", + fn: "dyn.mathFold", + args: [ + src, + { kind: "boolLit", value: name === "max", type: BOOL, loc }, + { kind: "strLit", value: what, type: STRING, loc }, + ], + type: F64, + loc, + }; + } if (src.type.kind !== "array" || src.type.elem.kind !== "f64") { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 3dc6dcebd..d8e78be08 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -671,6 +671,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.propertyIsEnumerableComputed", // the destructuring pack throws V8's TypeError on non-iterable dyn kinds "dyn.iterPack", + // ToNumber of each element can run valueOf + "dyn.mathFold", "dyn.destructureRest", "dyn.arrayFromIterator", "dyn.iterator", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..9dbcc1514 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -219,6 +219,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * not iterable (cannot read property Symbol(Symbol.iterator))"). In the * may-throw seed set. */ "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, + "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 }, "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json new file mode 100644 index 000000000..4d1376a12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7488-math-spread-untyped.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..100dd2295 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1273,6 +1273,9 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); +/* Math.max/Math.min(...src) over an untyped iterable; `what` names the + * source in V8's null/undefined spread TypeError. NaN with it pending. */ +double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what); /* JS String() over a dyn value (arrays join, [object Object], the error * encoding renders "name: message"); borrowed, result +1, never throws. */ ScrStr *scr_dyn_display(const ScrDyn *d); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..9abb7796b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1557,6 +1557,41 @@ static ScrDyn *scr_dyn_not_iterable(const ScrDyn *src, const ScrStr *msg) { return NULL; } +/* Math.max(...src) / Math.min(...src) over an untyped iterable: every + * value's ToNumber runs in order, then NaN poisons and ±0 orders as in + * Node. A non-iterable source throws V8's call-spread TypeError: `what` + * names the source for null and undefined, every other kind gets the + * Symbol.iterator wording. NaN with the exception pending when packing or + * a coercion throws. */ +double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what) { + if (src->kind == SCR_DYN_UNDEF || src->kind == SCR_DYN_NULL) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, what->data, what->len); + scr_jb_puts(&b, src->kind == SCR_DYN_NULL ? " is not iterable (cannot read property null)" + : " is not iterable (cannot read property undefined)"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NAN; + } + static const char spread_msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function"; + ScrStr *msg = scr_str_new(spread_msg, sizeof spread_msg - 1); + ScrDyn *pack = scr_dyn_iter_pack(src, msg); + scr_str_release(msg); + if (!pack) return NAN; + double best = max ? -INFINITY : INFINITY; + bool nan = false; + for (size_t i = 0; i < pack->v.arr.len; i++) { + double v = scr_dyn_number_coerce(pack->v.arr.items[i]); + if (scr_exc_pending()) break; + if (isnan(v)) nan = true; + else if (max ? (v > best || (v == 0.0 && best == 0.0 && !signbit(v))) + : (v < best || (v == 0.0 && best == 0.0 && signbit(v)))) + best = v; + } + scr_dyn_release(pack); + return nan ? NAN : best; +} + ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { if (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) return scr_dyn_handle_ops_of(src)->iter_pack(src->v.handle.ptr); diff --git a/tests/corpus/7488-math-spread-untyped.js b/tests/corpus/7488-math-spread-untyped.js new file mode 100644 index 000000000..a30ece320 --- /dev/null +++ b/tests/corpus/7488-math-spread-untyped.js @@ -0,0 +1,33 @@ +// Math.max/Math.min spreading an untyped iterable (zod's +// `Math.min(...caps.map((c) => c[0]))`). +function caps(list) { + return list.map((c) => c[0]); +} +console.log(Math.min(...caps([[5], [2], [9]])), Math.max(...caps([[5], [2], [9]]))); +console.log(Math.min(...caps([])), Math.max(...caps([]))); +console.log(Math.min(...caps([["3"], [true], [null]])), Math.max(...caps([["7"], [undefined]]))); +console.log(Object.is(Math.max(...caps([[-0], [0]])), 0), Object.is(Math.min(...caps([[0], [-0]])), -0)); +const order = []; +const valued = (n) => ({ valueOf() { order.push(n); return n; } }); +console.log(Math.max(...caps([[valued(1)], [NaN], [valued(3)]])), order.join(",")); +try { + Math.min(...caps([[{ valueOf() { throw new Error("boom"); } }]])); +} catch (e) { + console.log(e.message); +} +const sources = { none: undefined, nil: null, num: 5, obj: {}, str: "48" }; +function pick(k) { + return sources[k]; +} +for (const k of Object.keys(sources)) { + try { + console.log(Math.max(...sources[k])); + } catch (e) { + console.log(e.constructor.name, e.message); + } + try { + console.log(Math.min(...pick(k))); + } catch (e) { + console.log(e.constructor.name, e.message); + } +} diff --git a/tests/harness/corpus-costs/7488-math-spread-untyped.js.json b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json new file mode 100644 index 000000000..20168ce9a --- /dev/null +++ b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3800 } From 4b34943b1ad00c229bf2656d21c61c94a36997ba Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:03:00 -0700 Subject: [PATCH 07/30] Adopt closure arguments, add adoption, @types and failing-check cases Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/dyn.ts | 20 + .../src/frontend/decl-projection.test.ts | 33 + .../fixtures/npm/cases/types-boundary/main.ts | 19 + .../node_modules/@types/rowparse/index.d.ts | 12 + .../node_modules/@types/rowparse/package.json | 1 + .../node_modules/rowparse/index.js | 26 + .../node_modules/rowparse/package.json | 1 + .../npm/cases/types-boundary/package.json | 1 + .../fixtures/npm/cases/zod3-adoption/main.ts | 39 + .../zod3-adoption/node_modules/zod/LICENSE | 21 + .../node_modules/zod/dist/esm/index.js | 3 + .../node_modules/zod/dist/esm/package.json | 3 + .../node_modules/zod/dist/esm/v3/ZodError.js | 132 + .../node_modules/zod/dist/esm/v3/errors.js | 9 + .../node_modules/zod/dist/esm/v3/external.js | 6 + .../zod/dist/esm/v3/helpers/enumUtil.js | 1 + .../zod/dist/esm/v3/helpers/errorUtil.js | 6 + .../zod/dist/esm/v3/helpers/parseUtil.js | 109 + .../zod/dist/esm/v3/helpers/partialUtil.js | 1 + .../zod/dist/esm/v3/helpers/typeAliases.js | 1 + .../zod/dist/esm/v3/helpers/util.js | 133 + .../node_modules/zod/dist/esm/v3/index.js | 4 + .../zod/dist/esm/v3/locales/en.js | 107 + .../zod/dist/esm/v3/standard-schema.js | 1 + .../node_modules/zod/dist/esm/v3/types.js | 3691 +++++++++++++++++ .../node_modules/zod/dist/types/index.d.ts | 3 + .../zod/dist/types/v3/ZodError.d.ts | 164 + .../zod/dist/types/v3/errors.d.ts | 5 + .../zod/dist/types/v3/external.d.ts | 6 + .../zod/dist/types/v3/helpers/enumUtil.d.ts | 8 + .../zod/dist/types/v3/helpers/errorUtil.d.ts | 9 + .../zod/dist/types/v3/helpers/parseUtil.d.ts | 78 + .../dist/types/v3/helpers/partialUtil.d.ts | 8 + .../dist/types/v3/helpers/typeAliases.d.ts | 2 + .../zod/dist/types/v3/helpers/util.d.ts | 85 + .../node_modules/zod/dist/types/v3/index.d.ts | 4 + .../zod/dist/types/v3/locales/en.d.ts | 3 + .../zod/dist/types/v3/standard-schema.d.ts | 102 + .../node_modules/zod/dist/types/v3/types.d.ts | 1031 +++++ .../node_modules/zod/package.json | 120 + .../npm/cases/zod3-adoption/package.json | 1 + .../fixtures/npm/cases/zod3-boundary/lying.ts | 19 + tests/harness/decl-boundary.test.ts | 33 + 43 files changed, 6061 insertions(+) create mode 100644 packages/compiler/src/frontend/decl-projection.test.ts create mode 100644 tests/fixtures/npm/cases/types-boundary/main.ts create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js create mode 100644 tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json create mode 100644 tests/fixtures/npm/cases/types-boundary/package.json create mode 100644 tests/fixtures/npm/cases/zod3-adoption/main.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts create mode 100644 tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/cases/zod3-adoption/package.json create mode 100644 tests/fixtures/npm/cases/zod3-boundary/lying.ts create mode 100644 tests/harness/decl-boundary.test.ts diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 72e6c6c9e..9ce8922d5 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -4684,6 +4684,26 @@ export class LlDyn { }, "ptr null", ); + // Declaration-typed boundary programs: an argument package code + // passes keeps its identity (scr_dyn_decl_adopt). + const capsule = this.host.declBoundary?.() === true ? this.liveToDyn(B, p, a) : null; + if (capsule !== null) { + host.declare(`declare void @scr_dyn_decl_adopt(ptr, ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + B.line(`call void @scr_dyn_decl_adopt(ptr ${ad}, ptr ${capsule})`); + B.line(`call void @scr_dyn_release(ptr ${capsule})`); + this.pendingBail( + B, + "dfk.adopt", + () => { + t.params.slice(0, i + 1).forEach((q, j) => { + const name = j === i ? a : argNames[j]; + if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${name})`); + }); + }, + "ptr null", + ); + } argNames.push(a); } }); diff --git a/packages/compiler/src/frontend/decl-projection.test.ts b/packages/compiler/src/frontend/decl-projection.test.ts new file mode 100644 index 000000000..ef8771b06 --- /dev/null +++ b/packages/compiler/src/frontend/decl-projection.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "vitest"; +import { applyEdits, isDeclBoundaryFile, isDeclBoundaryTypeName } from "./decl-projection.js"; + +describe("decl-projection", () => { + test("applyEdits nests wraps that close at one position", () => { + const text = "const p = a.b;"; + // Outer wrap of `a.b` and inner wrap of `a`... closing at different ends; + // two wraps closing at the same position close the inner one first. + const out = applyEdits(text, [ + { start: 10, end: 10, text: "(" }, + { start: 13, end: 13, text: " as Outer)", rank: -10 }, + { start: 10, end: 10, text: "(" }, + { start: 13, end: 13, text: " as Inner)", rank: -9 }, + ]); + expect(out).toBe("const p = ((a.b as Inner) as Outer);"); + }); + + test("applyEdits keeps the first of overlapping replacements and every line", () => { + const text = "type A = z.infer;\nconst x = 1;"; + const out = applyEdits(text, [ + { start: 9, end: 26, text: "__scriptc_decl_0" }, + { start: 11, end: 16, text: "nope" }, + ]); + expect(out).toBe("type A = __scriptc_decl_0;\nconst x = 1;"); + }); + + test("names", () => { + expect(isDeclBoundaryTypeName("__scriptc_decl_3")).toBe(true); + expect(isDeclBoundaryTypeName("Product")).toBe(false); + expect(isDeclBoundaryFile("/app/src/__scriptc_decl_boundary__.d.ts")).toBe(true); + expect(isDeclBoundaryFile("/app/src/types.d.ts")).toBe(false); + }); +}); diff --git a/tests/fixtures/npm/cases/types-boundary/main.ts b/tests/fixtures/npm/cases/types-boundary/main.ts new file mode 100644 index 000000000..c33e6e9bf --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/main.ts @@ -0,0 +1,19 @@ +// A plain-JavaScript package typed by an @types package: rows the +// declarations type as plain data become native records; the cursor, a +// constructor-function instance its declarations describe with an +// interface, keeps its JavaScript-world typing. +import { parseRows, cursor, type Row } from "rowparse"; + +const rows: Row[] = parseRows("name,qty\napple,3\npear,5\nfig,2\n"); +let total = 0; +for (const r of rows) total += Number(r.cells["qty"]); +console.log("total", total, rows.length); +const widest = rows.reduce((w, r) => Math.max(w, r.width), 0); +console.log("widest", widest); +rows[0]!.cells["qty"] = "30"; +console.log("first", JSON.stringify(rows[0]), Object.keys(rows[1]!.cells).join("|")); +const sorted = [...rows].sort((a, b) => a.cells["name"]!.localeCompare(b.cells["name"]!)).map((r) => r.line); +console.log("sorted", sorted.join(",")); +const c = cursor("abcdef"); +c.pos = 2; +console.log("cursor", c.remaining(), c.text); diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts new file mode 100644 index 000000000..8e099794d --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/index.d.ts @@ -0,0 +1,12 @@ +export type Row = { + line: number; + cells: { [column: string]: string }; + width: number; +}; +export declare function parseRows(text: string): Row[]; +export interface Cursor { + text: string; + pos: number; + remaining(): number; +} +export declare function cursor(text: string): Cursor; diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json new file mode 100644 index 000000000..7ce3bcc0e --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/@types/rowparse/package.json @@ -0,0 +1 @@ +{ "name": "@types/rowparse", "version": "1.0.0", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js new file mode 100644 index 000000000..4a53c8481 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/index.js @@ -0,0 +1,26 @@ +"use strict"; +// Plain JavaScript: rows are built key by key, so inference sees nothing. +function Cursor(text) { + this.text = text; + this.pos = 0; +} +Cursor.prototype.remaining = function () { + return this.text.length - this.pos; +}; +function parseRows(text) { + var lines = String(text).split("\n").filter(Boolean); + var header = lines[0].split(","); + var rows = []; + for (var i = 1; i < lines.length; i++) { + var cells = lines[i].split(","); + var row = {}; + for (var j = 0; j < header.length; j++) row[header[j]] = cells[j]; + rows.push({ line: i, cells: row, width: cells.length }); + } + return rows; +} +function cursor(text) { + return new Cursor(text); +} +exports.parseRows = parseRows; +exports.cursor = cursor; diff --git a/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json new file mode 100644 index 000000000..da66597bc --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/node_modules/rowparse/package.json @@ -0,0 +1 @@ +{ "name": "rowparse", "version": "1.0.0", "main": "index.js" } diff --git a/tests/fixtures/npm/cases/types-boundary/package.json b/tests/fixtures/npm/cases/types-boundary/package.json new file mode 100644 index 000000000..bae20e44c --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary/package.json @@ -0,0 +1 @@ +{ "name": "types-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-adoption/main.ts b/tests/fixtures/npm/cases/zod3-adoption/main.ts new file mode 100644 index 000000000..6566d0e3a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/main.ts @@ -0,0 +1,39 @@ +// Adoption at the declaration-typed boundary: objects package code still +// references (a cache, weak keys, parsed documents, a cycle through an +// unknown field) keep their identity when they cross into typed code. +import { z } from "zod"; + +const Row = z.object({ id: z.number(), name: z.string(), tags: z.array(z.string()), meta: z.unknown() }); +type Row = z.infer; + +// A package-side cache: the same objects come back from every call. +const cache: unknown[] = []; +const weak = new WeakMap(); +const Cached = z.custom<{ id: number; tags: string[] }>().transform((v) => v); +for (let i = 0; i < 4; i++) { + const raw = JSON.parse(`{"id":${i},"name":"n${i}","tags":["a","b"],"meta":{"deep":[1,2,{"x":${i}}]}}`) as unknown; + cache.push(raw); + weak.set(raw as object, "w" + i); +} +let sum = 0; +for (let round = 0; round < 200; round++) { + for (let i = 0; i < cache.length; i++) { + const r: Row = Row.parse(cache[i]); + sum += r.id + r.tags.length; + const again = Row.parse(cache[i]); + if (round === 0) console.log("fresh", r === again, r.name, JSON.stringify(r.meta)); + } +} +console.log("sum", sum); +// The package keeps the source objects; z.custom passes them through. +const kept = cache.map((c) => Cached.parse(c)); +kept[0]!.tags.push("c"); +console.log("kept", JSON.stringify(cache[0]), kept[0] === Cached.parse(cache[0])); +console.log("weak", weak.get(cache[1] as object), weak.has(cache[2] as object)); +// A cycle through an unknown field. +const AsRow = z.custom(); +const cyc: Row = { id: 9, name: "c", tags: [], meta: null }; +cyc.meta = cyc; +const back = AsRow.parse(cyc); +back.id = 10; +console.log("cycle", cyc.id, back === cyc, back.meta === cyc); diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/index.js @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js new file mode 100644 index 000000000..c72ce5288 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/ZodError.js @@ -0,0 +1,132 @@ +import { util } from "./helpers/util.js"; +export const ZodIssueCode = util.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite", +]); +export const quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +export class ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + // eslint-disable-next-line ban/ban + Object.setPrototypeOf(this, actualProto); + } + else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || + function (issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } + else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } + else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } + else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } + else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + // if (typeof el === "string") { + // curr[el] = curr[el] || { _errors: [] }; + // } else if (typeof el === "number") { + // const errorArray: any = []; + // errorArray._errors = []; + // curr[el] = curr[el] || errorArray; + // } + } + else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } + else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +} +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js new file mode 100644 index 000000000..26fdf628a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/errors.js @@ -0,0 +1,9 @@ +import defaultErrorMap from "./locales/en.js"; +let overrideErrorMap = defaultErrorMap; +export { defaultErrorMap }; +export function setErrorMap(map) { + overrideErrorMap = map; +} +export function getErrorMap() { + return overrideErrorMap; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/external.js @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/enumUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js new file mode 100644 index 000000000..e1640d1a4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/errorUtil.js @@ -0,0 +1,6 @@ +export var errorUtil; +(function (errorUtil) { + errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + // biome-ignore lint: + errorUtil.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js new file mode 100644 index 000000000..91b2afb81 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/parseUtil.js @@ -0,0 +1,109 @@ +import { getErrorMap } from "../errors.js"; +import defaultErrorMap from "../locales/en.js"; +export const makeIssue = (params) => { + const { data, path, errorMaps, issueData } = params; + const fullPath = [...path, ...(issueData.path || [])]; + const fullIssue = { + ...issueData, + path: fullPath, + }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message, + }; + } + let errorMessage = ""; + const maps = errorMaps + .filter((m) => !!m) + .slice() + .reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage, + }; +}; +export const EMPTY_PATH = []; +export function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData: issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, // contextual error map is first priority + ctx.schemaErrorMap, // then schema-bound map if available + overrideMap, // then global override map + overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map + ].filter((x) => !!x), + }); + ctx.common.issues.push(issue); +} +export class ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + }); + } + return ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +} +export const INVALID = Object.freeze({ + status: "aborted", +}); +export const DIRTY = (value) => ({ status: "dirty", value }); +export const OK = (value) => ({ status: "valid", value }); +export const isAborted = (x) => x.status === "aborted"; +export const isDirty = (x) => x.status === "dirty"; +export const isValid = (x) => x.status === "valid"; +export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/partialUtil.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/typeAliases.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js new file mode 100644 index 000000000..b9641f29b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/helpers/util.js @@ -0,0 +1,133 @@ +export var util; +(function (util) { + util.assertEqual = (_) => { }; + function assertIs(_arg) { } + util.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util.assertNever = assertNever; + util.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util.getValidEnumValues = (obj) => { + const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const filtered = {}; + for (const k of validKeys) { + filtered[k] = obj[k]; + } + return util.objectValues(filtered); + }; + util.objectValues = (obj) => { + return util.objectKeys(obj).map(function (e) { + return obj[e]; + }); + }; + util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban + ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban + : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return undefined; + }; + util.isInteger = typeof Number.isInteger === "function" + ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban + : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator); + } + util.joinValues = joinValues; + util.jsonStringifyReplacer = (_, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util || (util = {})); +export var objectUtil; +(function (objectUtil) { + objectUtil.mergeShapes = (first, second) => { + return { + ...first, + ...second, // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +export const ZodParsedType = util.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set", +]); +export const getParsedType = (data) => { + const t = typeof data; + switch (t) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/index.js @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js new file mode 100644 index 000000000..3e97ad51a --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/locales/en.js @@ -0,0 +1,107 @@ +import { ZodIssueCode } from "../ZodError.js"; +import { util, ZodParsedType } from "../helpers/util.js"; +const errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } + else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } + else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } + else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } + else { + util.assertNever(issue.validation); + } + } + else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } + else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util.assertNever(issue); + } + return { message }; +}; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/standard-schema.js @@ -0,0 +1 @@ +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js new file mode 100644 index 000000000..de1e6213e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/esm/v3/types.js @@ -0,0 +1,3691 @@ +import { ZodError, ZodIssueCode, } from "./ZodError.js"; +import { defaultErrorMap, getErrorMap } from "./errors.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { DIRTY, INVALID, OK, ParseStatus, addIssueToContext, isAborted, isAsync, isDirty, isValid, makeIssue, } from "./helpers/parseUtil.js"; +import { util, ZodParsedType, getParsedType } from "./helpers/util.js"; +class ParseInputLazyPath { + constructor(parent, value, path, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } + else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +} +const handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } + else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + }, + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap, invalid_type_error, required_error, description } = params; + if (errorMap && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap) + return { errorMap: errorMap, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +export class ZodType { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return (ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }); + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent, + }, + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async, + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }; + } + catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true, + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) + ? { + value: result.value, + } + : { + issues: ctx.common.issues, + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true, + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data), + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } + else if (typeof message === "function") { + return message(val); + } + else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val), + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } + else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } + else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } + else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement }, + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + /** Alias of safeParseAsync */ + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data), + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform }, + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault, + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def), + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch, + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description, + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(undefined).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const cuidRegex = /^c[^\s-]{8,}$/i; +const cuid2Regex = /^[0-9a-z]+$/; +const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +// const uuidRegex = +// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; +const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +// from https://stackoverflow.com/a/46181/1550155 +// old version: too slow, didn't support unicode +// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +//old email regex +// const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i; +// eslint-disable-next-line +// const emailRegex = +// /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\])|(\[IPv6:(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))\])|([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])*(\.[A-Za-z]{2,})+))$/; +// const emailRegex = +// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; +// const emailRegex = +// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +// const emailRegex = +// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; +// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression +const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +let emojiRegex; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +// const ipv6Regex = +// /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; +const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// https://base64.guru/standards/base64url +const base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } + else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; // require seconds if precision is nonzero + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +export function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + // Convert base64url to base64 + const base64 = header + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(header.length + ((4 - (header.length % 4)) % 4), "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } + catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +export class ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx.parsedType, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message, + }); + } + status.dirty(); + } + } + else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "url") { + try { + new URL(input.data); + } + catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "trim") { + input.data = input.data.trim(); + } + else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } + else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } + else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message), + }); + } + _addCheck(check) { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message), + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options, + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message), + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options, + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message), + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex: regex, + ...errorUtil.errToObj(message), + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value: value, + position: options?.position, + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value: value, + ...errorUtil.errToObj(message), + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message), + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message), + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message), + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }], + }); + } + toLowerCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }], + }); + } + toUpperCase() { + return new ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }], + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + // base64url encoding is a modification of base64 that can safely be used in URLs and filenames + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +// https://stackoverflow.com/questions/3966484/why-does-modulus-operator-return-fractional-number-in-javascript/31711034#31711034 +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return (valInt % stepInt) / 10 ** decCount; +} +export class ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx.parsedType, + }); + return INVALID; + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodNumber({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message), + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value: value, + message: errorUtil.toString(message), + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message), + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message), + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || (ch.kind === "multipleOf" && util.isInteger(ch.value))); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } + else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +} +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } + catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = undefined; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } + else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType, + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + _addCheck(check) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +} +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params), + }); +}; +export class ZodBoolean extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params), + }); +}; +export class ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx.parsedType, + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_date, + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = undefined; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date", + }); + status.dirty(); + } + } + else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date", + }); + status.dirty(); + } + } + else { + util.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()), + }; + } + _addCheck(check) { + return new ZodDate({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message), + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message), + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +} +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params), + }); +}; +export class ZodSymbol extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params), + }); +}; +export class ZodUndefined extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params), + }); +}; +export class ZodNull extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params), + }); +}; +export class ZodAny extends ZodType { + constructor() { + super(...arguments); + // to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject. + this._any = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params), + }); +}; +export class ZodUnknown extends ZodType { + constructor() { + super(...arguments); + // required + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +} +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params), + }); +}; +export class ZodNever extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType, + }); + return INVALID; + } +} +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params), + }); +}; +export class ZodVoid extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType, + }); + return INVALID; + } + return OK(input.data); + } +} +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params), + }); +}; +export class ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: (tooSmall ? def.exactLength.value : undefined), + maximum: (tooBig ? def.exactLength.value : undefined), + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message, + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message, + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message, + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result) => { + return ParseStatus.mergeArray(status, result); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) }, + }); + } + max(maxLength, message) { + return new ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) }, + }); + } + length(len, message) { + return new ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) }, + }); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params), + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape, + }); + } + else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element), + }); + } + else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } + else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } + else { + return schema; + } +} +export class ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + this.nonstrict = this.passthrough; + // extend< + // Augmentation extends ZodRawShape, + // NewOutput extends util.flatten<{ + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }>, + // NewInput extends util.flatten<{ + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // }> + // >( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, + // UnknownKeys, + // Catchall, + // NewOutput, + // NewInput + // > { + // return new ZodObject({ + // ...this._def, + // shape: () => ({ + // ...this._def.shape(), + // ...augmentation, + // }), + // }) as any; + // } + /** + * @deprecated Use `.extend` instead + * */ + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] }, + }); + } + } + else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys, + }); + status.dirty(); + } + } + else if (unknownKeys === "strip") { + } + else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } + else { + // run catchall validation + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse(new ParseInputLazyPath(ctx, value, ctx.path, key) //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data, + }); + } + } + if (ctx.common.async) { + return Promise.resolve() + .then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet, + }); + } + return syncPairs; + }) + .then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new ZodObject({ + ...this._def, + unknownKeys: "strict", + ...(message !== undefined + ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError, + }; + return { + message: defaultError, + }; + }, + } + : {}), + }); + } + strip() { + return new ZodObject({ + ...this._def, + unknownKeys: "strip", + }); + } + passthrough() { + return new ZodObject({ + ...this._def, + unknownKeys: "passthrough", + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation, + }), + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape(), + }), + typeName: ZodFirstPartyTypeKind.ZodObject, + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new ZodObject({ + ...this._def, + catchall: index, + }); + } + pick(mask) { + const shape = {}; + for (const key of util.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + omit(mask) { + const shape = {}; + for (const key of util.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new ZodObject({ + ...this._def, + shape: () => shape, + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } + else { + newShape[key] = fieldSchema.optional(); + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + required(mask) { + const newShape = {}; + for (const key of util.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } + else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new ZodObject({ + ...this._def, + shape: () => newShape, + }); + } + keyof() { + return createZodEnum(util.objectKeys(this.shape)); + } +} +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params), + }); +}; +export class ZodUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + // return first issue-free validation if it exists + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + // add issues from dirty option + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + // return invalid + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }), + ctx: childCtx, + }; + })).then(handleResults); + } + else { + let dirty = undefined; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + parent: null, + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx, + }); + if (result.status === "valid") { + return result; + } + else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues) => new ZodError(issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors, + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +} +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params), + }); +}; +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +////////// ////////// +////////// ZodDiscriminatedUnion ////////// +////////// ////////// +///////////////////////////////////////////////////// +///////////////////////////////////////////////////// +const getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } + else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } + else if (type instanceof ZodLiteral) { + return [type.value]; + } + else if (type instanceof ZodEnum) { + return type.options; + } + else if (type instanceof ZodNativeEnum) { + // eslint-disable-next-line ban/ban + return util.objectValues(type.enum); + } + else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } + else if (type instanceof ZodUndefined) { + return [undefined]; + } + else if (type instanceof ZodNull) { + return [null]; + } + else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } + else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } + else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } + else { + return []; + } +}; +export class ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator], + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + // Get all the valid discriminator values + const optionsMap = new Map(); + // try { + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params), + }); + } +} +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } + else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util.objectKeys(b); + const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } + else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } + else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } + else { + return { valid: false }; + } +} +export class ZodIntersection extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types, + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), + ]).then(([left, right]) => handleParsed(left, right)); + } + else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + })); + } + } +} +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left: left, + right: right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params), + }); +}; +// type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export class ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType, + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array", + }); + status.dirty(); + } + const items = [...ctx.data] + .map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }) + .filter((x) => !!x); // filter nulls + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } + else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new ZodTuple({ + ...this._def, + rest, + }); + } +} +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params), + }); +}; +export class ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType, + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data, + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } + else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third), + }); + } + return new ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second), + }); + } +} +export class ZodMap extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType, + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])), + }; + }); + if (ctx.common.async) { + const finalMap = new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } + else { + const finalMap = new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +} +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params), + }); +}; +export class ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType, + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message, + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message, + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements) { + const parsedSet = new Set(); + for (const element of elements) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements) => finalizeSet(elements)); + } + else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) }, + }); + } + max(maxSize, message) { + return new ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) }, + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +} +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params), + }); +}; +export class ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType, + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error, + }, + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), defaultErrorMap].filter((x) => !!x), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error, + }, + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(async function (...args) { + const error = new ZodError([]); + const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me._def.returns._def.type + .parseAsync(result, params) + .catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } + else { + // Would love a way to avoid disabling this rule, but we need + // an alias (using an arrow function was what caused 2651). + // eslint-disable-next-line @typescript-eslint/no-this-alias + const me = this; + return OK(function (...args) { + const parsedArgs = me._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()), + }); + } + returns(returnType) { + return new ZodFunction({ + ...this._def, + returns: returnType, + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new ZodFunction({ + args: (args ? args : ZodTuple.create([]).rest(ZodUnknown.create())), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params), + }); + } +} +export class ZodLazy extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +} +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter: getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params), + }); +}; +export class ZodLiteral extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +} +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value: value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params), + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params), + }); +} +export class ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return ZodEnum.create(values, { + ...this._def, + ...newDef, + }); + } + exclude(values, newDef = this._def) { + return ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef, + }); + } +} +ZodEnum.create = createZodEnum; +export class ZodNativeEnum extends ZodType { + _parse(input) { + const nativeEnumValues = util.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type, + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues, + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +} +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values: values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params), + }); +}; +export class ZodPromise extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType, + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap, + }); + })); + } +} +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params), + }); +}; +export class ZodEffects extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects + ? this._def.schema.sourceType() + : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } + else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } + else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx, + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + // return value is ignored + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } + else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result, + })); + }); + } + } + util.assertNever(effect); + } +} +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params), + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params), + }); +}; +export { ZodEffects as ZodTransformer }; +export class ZodOptional extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(undefined); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params), + }); +}; +export class ZodNullable extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +} +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params), + }); +}; +export class ZodDefault extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + removeDefault() { + return this._def.innerType; + } +} +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params), + }); +}; +export class ZodCatch extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + // newCtx is used to not collect issues from inner types in ctx + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [], + }, + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx, + }, + }); + if (isAsync(result)) { + return result.then((result) => { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + }); + } + else { + return { + status: "valid", + value: result.status === "valid" + ? result.value + : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data, + }), + }; + } + } + removeCatch() { + return this._def.innerType; + } +} +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params), + }); +}; +export class ZodNaN extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType, + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +} +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params), + }); +}; +export const BRAND = Symbol("zod_brand"); +export class ZodBranded extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx, + }); + } + unwrap() { + return this._def.type; + } +} +export class ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } + else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + }; + return handleAsync(); + } + else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx, + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value, + }; + } + else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx, + }); + } + } + } + static create(a, b) { + return new ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline, + }); + } +} +export class ZodReadonly extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +} +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params), + }); +}; +//////////////////////////////////////// +//////////////////////////////////////// +////////// ////////// +////////// z.custom ////////// +////////// ////////// +//////////////////////////////////////// +//////////////////////////////////////// +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +export function custom(check, _params = {}, +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r) => { + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +export { ZodType as Schema, ZodType as ZodSchema }; +export const late = { + object: ZodObject.lazycreate, +}; +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { + ZodFirstPartyTypeKind["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +// requires TS 4.4+ +class Class { + constructor(..._) { } +} +const instanceOfType = ( +// const instanceOfType = any>( +cls, params = { + message: `Input not instance of ${cls.name}`, +}) => custom((data) => data instanceof cls, params); +const stringType = ZodString.create; +const numberType = ZodNumber.create; +const nanType = ZodNaN.create; +const bigIntType = ZodBigInt.create; +const booleanType = ZodBoolean.create; +const dateType = ZodDate.create; +const symbolType = ZodSymbol.create; +const undefinedType = ZodUndefined.create; +const nullType = ZodNull.create; +const anyType = ZodAny.create; +const unknownType = ZodUnknown.create; +const neverType = ZodNever.create; +const voidType = ZodVoid.create; +const arrayType = ZodArray.create; +const objectType = ZodObject.create; +const strictObjectType = ZodObject.strictCreate; +const unionType = ZodUnion.create; +const discriminatedUnionType = ZodDiscriminatedUnion.create; +const intersectionType = ZodIntersection.create; +const tupleType = ZodTuple.create; +const recordType = ZodRecord.create; +const mapType = ZodMap.create; +const setType = ZodSet.create; +const functionType = ZodFunction.create; +const lazyType = ZodLazy.create; +const literalType = ZodLiteral.create; +const enumType = ZodEnum.create; +const nativeEnumType = ZodNativeEnum.create; +const promiseType = ZodPromise.create; +const effectsType = ZodEffects.create; +const optionalType = ZodOptional.create; +const nullableType = ZodNullable.create; +const preprocessType = ZodEffects.createWithPreprocess; +const pipelineType = ZodPipeline.create; +const ostring = () => stringType().optional(); +const onumber = () => numberType().optional(); +const oboolean = () => booleanType().optional(); +export const coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true, + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })), +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export const NEVER = INVALID; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts new file mode 100644 index 000000000..8352ab713 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/index.d.ts @@ -0,0 +1,3 @@ +import z3 from "./v3/index.js"; +export * from "./v3/index.js"; +export default z3; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts new file mode 100644 index 000000000..4e71355a3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/ZodError.d.ts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type ZodParsedType } from "./helpers/util.js"; +import type { TypeOf, ZodType } from "./index.js"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + invalid_literal: "invalid_literal"; + custom: "custom"; + invalid_union: "invalid_union"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + unrecognized_keys: "unrecognized_keys"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + too_small: "too_small"; + too_big: "too_big"; + invalid_intersection_types: "invalid_intersection_types"; + not_multiple_of: "not_multiple_of"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts new file mode 100644 index 000000000..52eacf72e --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/errors.d.ts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.js"; +import defaultErrorMap from "./locales/en.js"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts new file mode 100644 index 000000000..f0a4be4c7 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/external.d.ts @@ -0,0 +1,6 @@ +export * from "./errors.js"; +export * from "./helpers/parseUtil.js"; +export * from "./helpers/typeAliases.js"; +export * from "./helpers/util.js"; +export * from "./types.js"; +export * from "./ZodError.js"; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/enumUtil.d.ts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/errorUtil.d.ts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts new file mode 100644 index 000000000..e4dc1d7b3 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/parseUtil.d.ts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js"; +import type { ZodParsedType } from "./util.js"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts new file mode 100644 index 000000000..d07657a25 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/partialUtil.d.ts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/typeAliases.d.ts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/helpers/util.d.ts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts new file mode 100644 index 000000000..a9dab5738 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/index.d.ts @@ -0,0 +1,4 @@ +import * as z from "./external.js"; +export * from "./external.js"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts new file mode 100644 index 000000000..b2bea1ec4 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/locales/en.d.ts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.js"; +declare const errorMap: ZodErrorMap; +export default errorMap; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/standard-schema.d.ts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts new file mode 100644 index 000000000..301e09128 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/dist/types/v3/types.d.ts @@ -0,0 +1,1031 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.js"; +import type { enumUtil } from "./helpers/enumUtil.js"; +import { errorUtil } from "./helpers/errorUtil.js"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.js"; +import type { partialUtil } from "./helpers/partialUtil.js"; +import type { Primitive } from "./helpers/typeAliases.js"; +import { util, type objectUtil } from "./helpers/util.js"; +import type { StandardSchemaV1 } from "./standard-schema.js"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + private _cached; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json new file mode 100644 index 000000000..e02b87614 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/node_modules/zod/package.json @@ -0,0 +1,120 @@ +{ + "name": "zod", + "version": "3.25.67", + "type": "module", + "author": "Colin McDonnell ", + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": ["dist", "v3", "v4", "v4-mini"], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": ["typescript", "schema", "validation", "type", "inference"], + "license": "MIT", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts", + "module": "./dist/esm/index.js", + "exports": { + ".": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v3": { + "import": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "@zod/source": "./src/index.ts", + "types": "./dist/types/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./v4": { + "import": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/esm/v4/index.js" + }, + "require": { + "@zod/source": "./src/v4/index.ts", + "types": "./dist/types/v4/index.d.ts", + "default": "./dist/cjs/v4/index.js" + } + }, + "./v4-mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/mini": { + "@zod/source": "./src/v4/mini/index.ts", + "types": "./dist/types/v4/mini/index.d.ts", + "import": "./dist/esm/v4/mini/index.js", + "require": "./dist/cjs/v4/mini/index.js" + }, + "./v4/core": { + "@zod/source": "./src/v4/core/index.ts", + "types": "./dist/types/v4/core/index.d.ts", + "import": "./dist/esm/v4/core/index.js", + "require": "./dist/cjs/v4/core/index.js" + }, + "./v4/locales": { + "@zod/source": "./src/v4/locales/index.ts", + "types": "./dist/types/v4/locales/index.d.ts", + "import": "./dist/esm/v4/locales/index.js", + "require": "./dist/cjs/v4/locales/index.js" + }, + "./v4/locales/*": { + "@zod/source": "./src/v4/locales/*", + "types": "./dist/types/v4/locales/*", + "import": "./dist/esm/v4/locales/*", + "require": "./dist/cjs/v4/locales/*" + }, + "./package.json": "./package.json" + }, + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./v3": "./src/v3/index.ts", + "./v4": "./src/v4/index.ts", + "./v4-mini": "./src/v4/mini/index.ts", + "./v4/core": "./src/v4/core/index.ts", + "./v4/locales": "./src/v4/locales/index.ts", + "./v4/locales/*": "./src/v4/locales/*" + }, + "sourceDialects": ["@zod/source"], + "exclude": ["./src/**/*.test.ts"] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "support": { + "backing": { + "npm-funding": true + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsx ./build.mts", + "postbuild": "pnpm biome check --write .", + "test:watch": "pnpm vitest", + "test": "pnpm vitest run", + "bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"", + "pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run" + } +} diff --git a/tests/fixtures/npm/cases/zod3-adoption/package.json b/tests/fixtures/npm/cases/zod3-adoption/package.json new file mode 100644 index 000000000..ff368ad87 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-adoption/package.json @@ -0,0 +1 @@ +{ "name": "zod3-adoption-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod3-boundary/lying.ts b/tests/fixtures/npm/cases/zod3-boundary/lying.ts new file mode 100644 index 000000000..107dda7d8 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/lying.ts @@ -0,0 +1,19 @@ +// A declaration that lies about a value: the boundary check throws a +// TypeError naming the path (native only; Node does not check). Run by +// tests/harness/decl-boundary.test.ts. +import { z } from "zod"; + +const Liar = z.custom<{ name: string; n: number }>(() => true); +try { + const v = Liar.parse({ name: "x", n: "not a number" }); + console.log("liar", v.n + 1); +} catch (e) { + console.log("liar threw", e instanceof TypeError, (e as Error).message); +} +const Frozen = z.custom<{ tags: string[] }>(); +try { + const f = Frozen.parse(Object.freeze(JSON.parse('{"tags":["a"]}'))); + console.log("frozen", f.tags.length); +} catch (e) { + console.log("frozen threw", (e as Error).message); +} diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts new file mode 100644 index 000000000..7f8e93c84 --- /dev/null +++ b/tests/harness/decl-boundary.test.ts @@ -0,0 +1,33 @@ +/* The declaration-typed boundary's checks (decl-projection.ts): a value + * zod's declarations type wrongly fails the boundary cast with a TypeError + * naming the path, and a value the native record cannot represent (a + * frozen object) fails with the reason. Node does not check, so this is a + * native-only test; npm.test.ts runs the differential cases + * (zod3-boundary, zod3-adoption, zod3-infer). */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/lying.ts"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/decl-boundary"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test("boundary casts throw a path TypeError for values the declarations misdescribe", { timeout: 600_000 }, async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + pending.child.stdin?.end(); + const { stdout } = await pending; + expect(stdout).toBe( + "liar threw true expected number at $.n, got string\n" + + "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", + ); +}); From 94173bebdc4480d661d5316e3ac96a6eb8374a33 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:12:16 -0700 Subject: [PATCH 08/30] Name the compiler's own shim files independently of the install path Programs that compile against a package shim (fs-extra, chalk, undici, hot-shots) produced IR and LLVM that depended on where the compiler was installed: lifted helper and closure names hashed the shim file's path relative to the entry, the shared-helper location pass ordered candidate sites by absolute path, and IR artifacts spelled every shim location as an absolute path. Two checkouts of the same sources therefore emitted different artifacts, and the zero-cost gate always reported fs-extra-package as differing. Files scriptc ships are now named by their place inside scriptc (scriptc:shims/...) in structural positions, in the shared-helper ordering, and in serialized IR artifacts. In-memory locations keep the real path, so diagnostics and source lookups are unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/dts-paths.ts | 25 +++++++++++++ .../compiler/src/frontend/lowering/lowerer.ts | 30 +++++++++++----- packages/compiler/src/index.ts | 9 ++--- packages/compiler/src/ir/serialize.test.ts | 21 +++++++++++ packages/compiler/src/ir/serialize.ts | 36 +++++++++++++++++-- packages/compiler/src/native/prepare.ts | 7 ++-- 6 files changed, 111 insertions(+), 17 deletions(-) diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts index 3b2f32413..009d1c333 100644 --- a/packages/compiler/src/frontend/dts-paths.ts +++ b/packages/compiler/src/frontend/dts-paths.ts @@ -34,6 +34,31 @@ export function packageShimRoot(): string { : join(installedDeclarationRoot, "shims"); } +/** The directories of files scriptc itself ships, each with the prefix + * that names its files independently of where scriptc is installed + * (compilerOwnedFileName). IR artifacts spell source locations in them this + * way (serializeModuleParts). */ +export function compilerOwnedFileRoots(): readonly { from: string; to: string }[] { + return [{ from: tsgoPath(packageShimRoot()) + "/", to: "scriptc:shims/" }]; +} + +/** The name of a file scriptc itself ships (a package shim or one of the + * shipped declaration files) that does not depend on where scriptc is + * installed: `scriptc:shims/` or + * `scriptc:`. Null for every other file. Names and + * artifacts that would otherwise spell the installation's absolute path use + * it, so two installations of one compiler emit the same output. */ +export function compilerOwnedFileName(file: string): string | null { + const path = tsgoPath(file); + for (const { from, to } of compilerOwnedFileRoots()) { + if (path.startsWith(from)) return to + path.slice(from.length); + } + for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) { + if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`; + } + return null; +} + /** Path of the shipped ambient declarations — the always-shipped CORE * (comptime, setTimeout). Part of EVERY program scriptc * builds, the project-world preflight program included. */ diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..60eba3107 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -184,7 +184,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; */ import { dirname, relative, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; -import { tsgoPath } from "../dts-paths.js"; +import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; import { isBundledShimForwarder } from "../package-shims.js"; import { erasedTypeOnlyImport } from "../type-only-imports.js"; @@ -3200,13 +3200,14 @@ export class Lowerer { /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ /** Files are named by their path relative to the entry's directory, so - * names do not depend on where the program sits, and default library - * files by their base name: two checkers install them in different - * directories. */ + * names do not depend on where the program sits, default library files + * by their base name (two checkers install them in different + * directories), and the files scriptc ships by their name inside scriptc, + * so names do not depend on where scriptc is installed either. */ readonly positions = new StructuralPositions((sf) => this.program.isSourceFileDefaultLibrary(sf) ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` - : relative(dirname(this.entry.fileName), sf.fileName), + : (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)), ); /** The outermost node whose token span is exactly `loc`, or null. */ @@ -3238,7 +3239,10 @@ export class Lowerer { * before it. */ closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { const node = this.nodeAtLoc(loc); - const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const where = + node === null + ? `${compilerOwnedFileName(loc.file) ?? loc.file}:${loc.start}:${loc.end}` + : this.positions.of(node); const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; const first = keyedHelperName(this, prefix, key); const seen = this.closureNameCounts.get(first) ?? 0; @@ -3379,8 +3383,18 @@ export class Lowerer { if (interned.has(fn.name)) this.sharedHelpers.add(fn); if (this.sharedHelpers.size === 0) return; const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn])); - const earlier = (left: SrcLoc, right: SrcLoc): boolean => - left.file < right.file || (left.file === right.file && left.start < right.start); + // Files scriptc ships compare by their name inside scriptc, so the + // choice does not depend on where scriptc is installed. + const fileKeys = new Map(); + const fileKey = (file: string): string => { + let key = fileKeys.get(file); + if (key === undefined) fileKeys.set(file, (key = compilerOwnedFileName(file) ?? file)); + return key; + }; + const earlier = (left: SrcLoc, right: SrcLoc): boolean => { + if (left.file === right.file) return left.start < right.start; + return fileKey(left.file) < fileKey(right.file); + }; // The earliest use of each shared helper outside the shared helpers, and // which shared helpers each shared helper uses. const uses = new Map(); diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index f0f19a408..daa5af366 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -126,6 +126,7 @@ import { moduleRuntimeFeatures } from "./ir/runtime-features.js"; import { serializeModuleParts } from "./ir/serialize.js"; import { readTextParts, writeTextParts } from "./text-parts.js"; import { validateModule } from "./ir/validate.js"; +import { compilerOwnedFileRoots } from "./frontend/dts-paths.js"; import { loadProgram } from "./frontend/program-node.js"; import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js"; import { @@ -785,7 +786,7 @@ async function prepareExecutableInput( if (outputKind === "ir") { await mkdir(dirname(opts.outPath), { recursive: true }); - await writeTextParts(opts.outPath, serializeModuleParts(mod)); + await writeTextParts(opts.outPath, serializeModuleParts(mod, compilerOwnedFileRoots())); return { ok: true, artifact: { kind: "ir", path: opts.outPath } }; } @@ -885,7 +886,7 @@ async function prepareExecutableInput( if (opts.emitIr) { irPath = defaultSourcePaths.ir; irSourcePath = privateSiblingPath(irPath, "ir"); - await writeTextParts(irSourcePath, serializeModuleParts(mod)); + await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots())); } const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release"); @@ -1986,7 +1987,7 @@ async function emitSemanticLibraryHit( let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); - await writeTextParts(irPath, serializeModuleParts(mod)); + await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots())); } await compileLibraryNative(profile, llvmPath, archivePath, opts.sanitize ?? false, hit.native); timing("native-archive"); @@ -2272,7 +2273,7 @@ async function compileLibraryTracked( let irPath: string | undefined; if (opts.emitIr) { irPath = join(opts.outDir, `${stem}.lib.ir.json`); - await writeTextParts(irPath, serializeModuleParts(mod)); + await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots())); } const nativeFeatures = libraryNativeFeatures(mod, profile.emission); diff --git a/packages/compiler/src/ir/serialize.test.ts b/packages/compiler/src/ir/serialize.test.ts index 8345ab10a..c24e1f198 100644 --- a/packages/compiler/src/ir/serialize.test.ts +++ b/packages/compiler/src/ir/serialize.test.ts @@ -198,3 +198,24 @@ test("byte decoding keeps the whole-document errors and version refusal", () => const sentinel = text.replace('"nan"', '"future"'); expect(() => deserializeModuleBytes(Buffer.from(sentinel))).toThrow("Invalid IR number sentinel"); }); + +test("file roots respell the source locations under them and nothing else", () => { + const mod = numbers([1, 2]); + const shim = { file: '/opt/scriptc "a"/shims/pkg/index.ts', start: 3, end: 4 }; + const other = { file: "/opt/elsewhere/shims/pkg/index.ts", start: 5, end: 6 }; + const body = mod.functions[0]!.body as IrStmt[]; + body.push( + { kind: "exprStmt", expr: { kind: "numLit", value: 3, type: F64, loc: shim }, loc: shim }, + { kind: "exprStmt", expr: { kind: "numLit", value: 4, type: F64, loc: other }, loc: other }, + ); + const roots = [{ from: '/opt/scriptc "a"/shims/', to: "scriptc:shims/" }]; + const relocated = deserializeModule(serializeModuleParts(mod, roots).join("")); + const files = (relocated.functions[0]!.body as IrStmt[]).map((stmt) => stmt.loc.file); + expect(files).toEqual([ + "numbers.ts", + "numbers.ts", + "scriptc:shims/pkg/index.ts", + "/opt/elsewhere/shims/pkg/index.ts", + ]); + expect(serializeModuleParts(mod).join("")).toBe(serializeModule(mod, true)); +}); diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index ca5eb955e..314f258d6 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -46,11 +46,41 @@ function encodeFunction(fn: IrFunction): string | undefined { return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber); } +/** A directory whose source location files an IR artifact names by a + * prefix of its own: a location file `from + rest` serializes as + * `to + rest`. */ +export interface SerializedFileRoot { + from: string; + to: string; +} + +/** Respells the source location files under `roots` in serialized parts. + * A location's file is the first member of its JSON object, and the + * escaped spelling of a path prefix is the prefix of the escaped path. */ +function relocateFiles(parts: string[], roots: readonly SerializedFileRoot[]): string[] { + const needles = roots.map(({ from, to }) => ({ + from: `"file":${JSON.stringify(from).slice(0, -1)}`, + to: `"file":${JSON.stringify(to).slice(0, -1)}`, + })); + return parts.map((part) => + needles.reduce( + (text, { from, to }) => (text.includes(from) ? text.replaceAll(from, to) : text), + part, + ), + ); +} + /** The compact encoding as ordered parts: the module header up to the * function array, one part per function (with separate comma parts), and the * header's remainder. Concatenating the parts yields exactly - * `serializeModule(mod, true)`; writers stream them to disk in batches. */ -export function serializeModuleParts(mod: IrModule): string[] { + * `serializeModule(mod, true)`; writers stream them to disk in batches. + * `fileRoots` respells the source locations in those directories, so + * artifacts do not depend on where the compiler that wrote them is + * installed. */ +export function serializeModuleParts( + mod: IrModule, + fileRoots: readonly SerializedFileRoot[] = [], +): string[] { // A replacer reads through live native views. Serializing the complete // function array through one view refreshes every function capsule for // each element, making large compiler artifacts quadratic. The sentinel @@ -65,7 +95,7 @@ export function serializeModuleParts(mod: IrModule): string[] { parts.push(encodeFunction(fn) ?? "null"); } parts.push("]" + header.slice(offset + slot.length)); - return parts; + return fileRoots.length === 0 ? parts : relocateFiles(parts, fileRoots); } /** Compiler artifacts use compact JSON; API consumers can retain the diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index a2c627edf..da90dc2ce 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -26,6 +26,7 @@ import { type FrontendInputExclusions, trackedReadFile, } from "../frontend/input-tracker.js"; +import { compilerOwnedFileRoots } from "../frontend/dts-paths.js"; import { provenanceSources } from "../frontend/provenance-registry.js"; import { processFork } from "../frontend/process-fork.js"; import { prepareLibrary } from "../library/prepare.js"; @@ -312,7 +313,7 @@ export function prepareNativeExecutable( if (irInput !== "") timing("ir-input"); const ir = outputKind === "ir" || options.emitIr - ? batchTextParts(serializeModuleParts(prepared.mod)) + ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots())) : null; timing("ir-serialize"); const llvm = @@ -445,7 +446,9 @@ export function prepareNativeLibrary( const input: NativeExecutableInput = { ok: true, llvm, - ir: options.emitIr ? batchTextParts(serializeModuleParts(prepared.mod)) : null, + ir: options.emitIr + ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots())) + : null, sidecarJson: prepared.sidecarJson, features: executableLinkFeatures(prepared.mod), sources: [...prepared.sourceTexts], From 74f2c9754b4540fe67703c1cda5d4b33adc033e4 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:03:18 -0700 Subject: [PATCH 09/30] Dispatch String-only method names on untyped receivers by runtime kind A call of a String.prototype-only method name (trim, toLowerCase, startsWith, ...) on a checker-untyped receiver was lowered as a string call that throws unless the receiver is a string. A checked-dynamic object can carry its own member of that name: zod's schemas assign `trim` in their constructors, so `z.string().trim().min(1)` typed the `trim()` result as a string and refused `.min` with SC1090. Untyped receivers now take the runtime-kind dispatch that object-typed receivers missing the member already use: a string runs the string lowering, any other value calls its own member as Node does. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-calls.ts | 23 ++++++----- ...d-receiver-own-string-method-names.ts.json | 6 +++ ...ntyped-receiver-own-string-method-names.ts | 38 +++++++++++++++++++ ...d-receiver-own-string-method-names.ts.json | 1 + 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json create mode 100644 tests/corpus/7441-untyped-receiver-own-string-method-names.ts create mode 100644 tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c1bc044ae..784efeb13 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3797,18 +3797,21 @@ function lowerDynReceiverMethodCall( } if (access.name.text === "flatMap") return lowerDynArrayFlatMapCall(lowerer, call, access, recv); // String methods claim only names NO other dyn-representable kind's - // prototype declares (Array carries includes/indexOf/slice too): for - // these, "the receiver is a string, or the call throws V8's TypeError" - // IS Node's semantics for every possible dyn value. Shared names would - // need a receiver-kind dispatch — they keep the fence. + // prototype declares (Array carries includes/indexOf/slice too), so no + // other kind inherits them. Shared names would need a receiver-kind + // dispatch — they keep the fence. if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { - // An object-typed receiver the member is missing from (a JavaScript - // module object whose members are assigned in a function body, - // `common.repeat(" ", n)`) holds a string or an object with that - // member: dispatch on the receiver's runtime kind. + // A checked-dynamic object can still carry its OWN member of that + // name: zod's schemas assign `trim`, `toLowerCase` and `startsWith` + // in their constructors (`z.string().trim().min(1)`), and a JavaScript + // module object can assign `repeat` in a function body + // (`common.repeat(" ", n)`, an object-typed receiver the member is + // missing from). An untyped receiver, or a member the checker cannot + // type, dispatches on the receiver's runtime kind: a string runs the + // string lowering, anything else calls its member as Node does. if ( - !checkerUntyped && - (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && !call.questionDotToken && !access.questionDotToken && !call.arguments.nodes.some(ts.isSpreadElement) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json new file mode 100644 index 000000000..7477193ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7441-untyped-receiver-own-string-method-names.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7441-untyped-receiver-own-string-method-names.ts b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts new file mode 100644 index 000000000..632ceb16f --- /dev/null +++ b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts @@ -0,0 +1,38 @@ +// An untyped receiver calling a String.prototype method name holds a +// string or a checked-dynamic object with its own method of that name +// (zod's schemas assign `trim`, `toLowerCase` and `startsWith` in their +// constructors, so `z.string().trim().min(1)` chains schema calls). +// Each call dispatches on the receiver's runtime kind. +interface Field { + value: string; + checks: string[]; +} + +function field(value: string, checks: string[]): any { + const self: Field & Record = { value, checks }; + self["trim"] = () => field(value.trim(), [...checks, "trim"]); + self["toLowerCase"] = () => field(value.toLowerCase(), [...checks, "lower"]); + self["startsWith"] = (prefix: string) => checks.length > 0 && value.startsWith(prefix); + self["min"] = (n: number) => field(value, [...checks, `min(${n})`]); + return self; +} + +const chained = field(" Hello World ", []).trim().toLowerCase().min(1); +console.log(JSON.stringify(chained.value), chained.checks.join(",")); +console.log(field("abc", ["x"]).startsWith("ab"), field("abc", []).startsWith("ab")); + +const text: any = " Mixed Case "; +console.log(JSON.stringify(text.trim()), text.toLowerCase(), text.startsWith(" M")); + +const parsed: any = JSON.parse('{"name":" padded "}'); +console.log(JSON.stringify(parsed.name.trim())); + +for (const value of [field(" a ", []), " b ", 42, null]) { + const v: any = value; + try { + const out = v.trim(); + console.log(typeof out === "string" ? JSON.stringify(out) : `field ${JSON.stringify(out.value)}`); + } catch (error) { + console.log(error instanceof TypeError, (error as Error).message); + } +} diff --git a/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json new file mode 100644 index 000000000..615c617ae --- /dev/null +++ b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json @@ -0,0 +1 @@ +{ "plain": 5800 } From 14966de563791e76de1bd455450e496e0838ee0f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:35:40 -0700 Subject: [PATCH 10/30] Move adoption into its own runtime unit behind the declAdopt feature Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 1 + .../src/backend/native-link-info-core.ts | 1 + .../compiler/src/backend/native/contracts.ts | 1 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../src/executable/executable-cache.ts | 1 + .../src/frontend/decl-projection.test.ts | 19 +- .../compiler/src/frontend/decl-projection.ts | 80 ++++++-- .../frontend/lowering/coercions/structural.ts | 10 +- .../lowering/expressions/narrowing.ts | 20 +- packages/compiler/src/frontend/pipeline.ts | 12 +- packages/compiler/src/frontend/program.ts | 19 +- packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/runtime-features.ts | 5 + .../compiler/src/native/sanitizer.test.ts | 1 + .../npm/cases/types-boundary/main.ts.json | 6 + .../npm/cases/zod3-adoption/main.ts.json | 6 + .../npm/cases/zod3-boundary/main.ts.json | 6 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_decl_adopt.c | 149 +++++++++++++++ packages/runtime/src/scr_dyn.h | 5 - packages/runtime/src/scr_json.c | 176 ++---------------- 22 files changed, 317 insertions(+), 207 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json create mode 100644 packages/runtime/src/scr_decl_adopt.c diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 672fddd96..32318d53e 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.handleProps ? { handleProps: true } : {}), + ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 5782f091d..e018037fd 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -49,6 +49,7 @@ export interface NativeLinkFeatures { /** Own properties JavaScript adds to children: scr_dyn_handle_props.c. * Absent (false) for programs without them. */ handleProps?: boolean; + declAdopt?: boolean; /** for await over checked-dynamic values: scr_dyn_for_await.c. Absent * (false) for programs without it. */ dynForAwait?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index f683ede3e..61de82e72 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -122,6 +122,7 @@ export interface CcOptions { streamDyn?: boolean; /** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */ handleProps?: boolean; + declAdopt?: boolean; /** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */ dynForAwait?: boolean; /** events.once with options (events.onceSignal): compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 1b90bb1f1..233b9cac2 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -279,6 +279,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"), // Own properties JavaScript adds to children. unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"), + // The declaration-typed boundary's adoption. + unit((u) => u.opts.declAdopt === true, "scr_decl_adopt.c"), // for await over checked-dynamic values. unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"), // for await over checked-dynamic values that may hold native streams. diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index b59fdc093..75953be59 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -86,6 +86,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { dynTier: boolean; streamDyn: boolean; handleProps: boolean; + declAdopt: boolean; dynForAwait: boolean; eventsOnceSignal: boolean; dynBuffer: boolean; @@ -283,6 +284,7 @@ export function effectiveRuntimeFeatures( dynTier: features.dynTier === true, streamDyn: features.streamDyn === true, handleProps: features.handleProps === true, + declAdopt: features.declAdopt === true, dynForAwait: features.dynForAwait === true, eventsOnceSignal: features.eventsOnceSignal === true, dynBuffer: features.dynBuffer === true, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 5c3904a91..7b7febc06 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -70,6 +70,7 @@ export interface EarlyExecutableNativeFeatures { dynTier?: boolean; streamDyn?: boolean; handleProps?: boolean; + declAdopt?: boolean; dynForAwait?: boolean; eventsOnceSignal?: boolean; dynBuffer?: boolean; diff --git a/packages/compiler/src/frontend/decl-projection.test.ts b/packages/compiler/src/frontend/decl-projection.test.ts index ef8771b06..dee695dd8 100644 --- a/packages/compiler/src/frontend/decl-projection.test.ts +++ b/packages/compiler/src/frontend/decl-projection.test.ts @@ -2,17 +2,16 @@ import { describe, expect, test } from "vitest"; import { applyEdits, isDeclBoundaryFile, isDeclBoundaryTypeName } from "./decl-projection.js"; describe("decl-projection", () => { - test("applyEdits nests wraps that close at one position", () => { - const text = "const p = a.b;"; - // Outer wrap of `a.b` and inner wrap of `a`... closing at different ends; - // two wraps closing at the same position close the inner one first. - const out = applyEdits(text, [ - { start: 10, end: 10, text: "(" }, - { start: 13, end: 13, text: " as Outer)", rank: -10 }, - { start: 10, end: 10, text: "(" }, - { start: 13, end: 13, text: " as Inner)", rank: -9 }, + test("applyEdits closes the wrap that opened last first", () => { + // `x.y` wrapped from 0 (outer) and from 2 (inner), both closing at 5: + // closing parts rank by their wrap's negated start. + const out = applyEdits("x = a.b;", [ + { start: 4, end: 4, text: "(" }, + { start: 7, end: 7, text: " as Outer)", rank: -4 }, + { start: 6, end: 6, text: "(" }, + { start: 7, end: 7, text: " as Inner)", rank: -6 }, ]); - expect(out).toBe("const p = ((a.b as Inner) as Outer);"); + expect(out).toBe("x = (a.(b as Inner) as Outer);"); }); test("applyEdits keeps the first of overlapping replacements and every line", () => { diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 923bfb428..4ff843c8b 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -316,7 +316,8 @@ function isWriteTarget(node: ts.Node): boolean { return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken; } if (ts.isDeleteExpression(parent)) return true; - if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) return parent.initializer === child; + if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) + return parent.initializer === child; if (ts.isShorthandPropertyAssignment(parent)) return true; if ( ts.isArrayLiteralExpression(parent) || @@ -325,9 +326,19 @@ function isWriteTarget(node: ts.Node): boolean { ) { // Destructuring assignment targets: conservative, skip. let up: ts.Node | undefined = parent; - while (up !== undefined && (ts.isArrayLiteralExpression(up) || ts.isObjectLiteralExpression(up) || ts.isPropertyAssignment(up) || ts.isSpreadElement(up))) + while ( + up !== undefined && + (ts.isArrayLiteralExpression(up) || + ts.isObjectLiteralExpression(up) || + ts.isPropertyAssignment(up) || + ts.isSpreadElement(up)) + ) up = up.parent; - if (up !== undefined && ts.isBinaryExpression(up) && up.operatorToken.kind === ts.SyntaxKind.EqualsToken) + if ( + up !== undefined && + ts.isBinaryExpression(up) && + up.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) return true; } return false; @@ -469,21 +480,27 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => { if (!ts.isExpression(jsNode)) return; const isCall = - ts.isCallExpression(jsNode) || ts.isNewExpression(jsNode) || ts.isTaggedTemplateExpression(jsNode); - const isAccess = ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); + ts.isCallExpression(jsNode) || + ts.isNewExpression(jsNode) || + ts.isTaggedTemplateExpression(jsNode); + const isAccess = + ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode); const isAwait = ts.isAwaitExpression(jsNode); if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return; if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return; if (inOptionalChain(jsNode)) return; const parent = jsNode.parent; if (parent !== undefined && ts.isExpressionStatement(parent)) return; - if (parent !== undefined && (ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent))) + if ( + parent !== undefined && + (ts.isImportSpecifier(parent) || + ts.isImportClause(parent) || + ts.isNamespaceImport(parent) || + ts.isExportSpecifier(parent)) + ) return; if (ts.isIdentifier(jsNode)) { - if ( - parent !== undefined && - (ts.isPropertyAccessExpression(parent) && parent.name === jsNode) - ) + if (parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.name === jsNode) return; } const jsType = typeAt(jsChecker, jsNode); @@ -556,7 +573,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const annotated = new Set(); const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => { - if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) return; + if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) + return; const fn = jsNode.parent; if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return; if (jsNode.dotDotDotToken !== undefined) return; @@ -570,7 +588,13 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression); if (ctxFn === undefined) return; const sigs = declChecker.getCallSignatures(ctxFn); - if (!sigs.some((s) => s.declaration !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)))) + if ( + !sigs.some( + (s) => + s.declaration !== undefined && + isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration)), + ) + ) return; } catch { return; @@ -582,7 +606,10 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const nameStart = ts.tokenStartOfNode(name); const text = jsSf.text; const bareArrow = - ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && text[ts.tokenStartOfNode(fn)] !== "(" && text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === "" ; + ts.isArrowFunction(fn) && + fn.parameters.nodes.length === 1 && + text[ts.tokenStartOfNode(fn)] !== "(" && + text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === ""; const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; if (bareArrow) { fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); @@ -633,7 +660,11 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const decls = sym.declarations; if (decls.length === 1) { const decl = decls[0]!; - if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined && decl.type === undefined) + if ( + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + decl.type === undefined + ) result = willBeTyped(decl.initializer, depth + 1); else if (ts.isParameter(decl) && annotated.has(decl)) result = true; else if (ts.isBindingElement(decl)) result = false; @@ -675,10 +706,17 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | return found; }; for (const e of kept) { - if (!ts.isAwaitExpression(e) && containsKept(e) && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))) { + if ( + !ts.isAwaitExpression(e) && + containsKept(e) && + (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) + ) { // `a.b.c` with `a.b` wrapped: the outer access reads typed data. const recv = receiverOf(e); - if (recv !== null && (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv))) + if ( + recv !== null && + (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv)) + ) continue; } const alias = exprCandidates.get(e)!; @@ -689,13 +727,19 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // semicolon continues the previous expression: guard it. let up: ts.Node | undefined = e; while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; - if (up?.parent !== undefined && ts.isExpressionStatement(up.parent) && ts.tokenStartOfNode(up.parent) === start) + if ( + up?.parent !== undefined && + ts.isExpressionStatement(up.parent) && + ts.tokenStartOfNode(up.parent) === start + ) prefix = ";("; void stmt; fileEdits.push({ start, end: start, text: prefix }); // Closing parts at one position close inner wraps first. fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start }); - plan.push(`${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`); + plan.push( + `${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`, + ); } if (fileEdits.length === 0) continue; diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 58992da54..4321a5ead 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -290,7 +290,11 @@ export function applyWidthLift( throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot"); // The declaration-typed boundary keeps user values' identity when // they cross into package code (decl-projection.ts). - if (lowerer.declBoundary && !/[\\/]node_modules[\\/]/.test(loc.file) && liveBoxable(lowerer, value.type)) + if ( + lowerer.declBoundary && + !/[\\/]node_modules[\\/]/.test(loc.file) && + liveBoxable(lowerer, value.type) + ) return { kind: "dynFrom", value, liveRef: true, type: DYN, loc }; return { kind: "dynFrom", value, type: DYN, loc }; } @@ -790,5 +794,7 @@ function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set()): boo if (t.kind === "array") return true; if (t.kind !== "union" || seen.has(t.unionId)) return false; seen.add(t.unionId); - return lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false; + return ( + lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false + ); } diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index d0064e56c..fa8ad686f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -1618,7 +1618,9 @@ function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set()): if (t.kind === "record" || t.kind === "array") return true; if (t.kind !== "union" || seen.has(t.unionId)) return false; seen.add(t.unionId); - return lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false; + return ( + lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false + ); } /** `( as __scriptc_decl_N)`: check the dynamic value against @@ -1654,7 +1656,13 @@ function declBoundaryCast( { kind: "varDecl", localId: value.id, - init: { kind: "dynCheck", value: varRef(source.id, DYN, loc), adopt: true, type: target, loc }, + init: { + kind: "dynCheck", + value: varRef(source.id, DYN, loc), + adopt: true, + type: target, + loc, + }, loc, }, { @@ -1664,7 +1672,13 @@ function declBoundaryCast( fn: "dyn.declAdopt", args: [ varRef(source.id, DYN, loc), - { kind: "dynFrom", value: varRef(value.id, target, loc), liveRef: true, type: DYN, loc }, + { + kind: "dynFrom", + value: varRef(value.id, target, loc), + liveRef: true, + type: DYN, + loc, + }, ], type: VOID, loc, diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 193e8adc0..6da81dfbc 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -685,8 +685,8 @@ function loadFrontend( ...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile), ].map((sf) => [sf.fileName, sf.text]), ), - lower: (opts) => - lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { + lower: (opts) => { + const lowered = lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, @@ -695,7 +695,13 @@ function loadFrontend( projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, - }), + }); + // The declaration-typed boundary's notes (SC6102) join the + // performance warnings; they never fail a build. + const notes = finalLoad.declBoundaryNotes; + if (notes === undefined) return lowered; + return { ...lowered, performance: [...notes, ...(lowered.performance ?? [])] }; + }, npmStatic: statuses, npmImportSites: npmSites, dispose: finalLoad.dispose, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index ef5e46d50..68b649b07 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -387,6 +387,9 @@ export interface LoadResult { * checkPreflight. */ startupCrash?: StartupCrash | null; configDiags: ScrDiagnostic[]; + /** SC6102 performance notes of the declaration-typed boundary + * (decl-projection.ts): files whose projection was dropped. */ + declBoundaryNotes?: ScrDiagnostic[]; /** The files whose JSX the program lowers (jsx-source.ts), by file name. * The program holds their rewritten text; positions in diagnostics refer * to it. */ @@ -932,6 +935,7 @@ function loadProgram7( // lowering compiles holds them with their JSX lowered. const jsx = lowerProgramJsx7(program, options); let tscWorld: LoadResult["tscWorld"]; + const declBoundaryNotes: ScrDiagnostic[] = []; if (jsx.refusal !== null) { config.diags.push(jsx.refusal); } else if (jsx.sources.size > 0) { @@ -977,9 +981,7 @@ function loadProgram7( if (dropped.length > 0) { program.dispose(); program = host.createProgram([...programRoots, overridesDtsPath()], options); - for (const file of dropped) { - config.diags.push(declBoundaryDroppedDiag(file)); - } + for (const file of dropped) declBoundaryNotes.push(declBoundaryDroppedDiag(file)); } if (dropped.length === projection.files.size) { program.dispose(); @@ -1004,6 +1006,7 @@ function loadProgram7( entry, moduleOrder: [], configDiags: config.diags, + ...(declBoundaryNotes.length > 0 ? { declBoundaryNotes } : {}), typeWorld: { nodeFallback: nodeTypes === null, projectLib: config.projectLib ?? null, @@ -1299,7 +1302,15 @@ function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]) continue; if (name.endsWith(".tsx")) continue; // JSX files: not projected yet const text = sf.text; - if (packages.some((pkg) => text.includes(`"${pkg}"`) || text.includes(`'${pkg}'`) || text.includes(`"${pkg}/`) || text.includes(`'${pkg}/`))) + if ( + packages.some( + (pkg) => + text.includes(`"${pkg}"`) || + text.includes(`'${pkg}'`) || + text.includes(`"${pkg}/`) || + text.includes(`'${pkg}/`), + ) + ) out.push(sf.fileName); } return out; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index f0f19a408..26ed4defb 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -600,6 +600,7 @@ async function compileExecutableNative( ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.handleProps ? { handleProps: true } : {}), + ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85910369a..819cfbee0 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -858,6 +858,10 @@ const RUNTIME_FEATURE_RULES = { }), /** Own properties JavaScript adds to children (scr_dyn_handle_props.c). */ handleProps: rule({ fns: ["child.propsInstall"] }), + /** The declaration-typed boundary's adoption (scr_decl_adopt.c): its + * casts, and closures boxed in a boundary program, which adopt their + * arguments. */ + declAdopt: rule({ fns: ["dyn.declAdopt"], initial: (mod) => mod.declBoundary === true }), /** for await over checked-dynamic values (scr_dyn_for_await.c). */ dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }), /** Buffer.prototype methods called on checked-dynamic values @@ -1081,6 +1085,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stream: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, dynBuffer: false, builtinValues: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 49867f0ad..4c031a8e7 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -79,6 +79,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dynTier: false, streamDyn: false, handleProps: false, + declAdopt: false, dynForAwait: false, eventsOnceSignal: false, dynBuffer: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json new file mode 100644 index 000000000..0ef25fcbd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json new file mode 100644 index 000000000..72fa6d390 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-adoption/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-adoption/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json new file mode 100644 index 000000000..4b8187352 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod3-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod3-boundary/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index d0194be05..285e767f4 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -127,6 +127,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_stream_dyn.c", "streamDyn"], // Own properties JavaScript adds to children. ["scr_dyn_handle_props.c", "handleProps"], + ["scr_decl_adopt.c", "declAdopt"], // for await over checked-dynamic values. ["scr_dyn_for_await.c", "dynForAwait"], ["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")], diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c new file mode 100644 index 000000000..3173ffb84 --- /dev/null +++ b/packages/runtime/src/scr_decl_adopt.c @@ -0,0 +1,149 @@ +/* The declaration-typed boundary's adoption, linked only by programs with + * boundary casts (runtime feature declAdopt). + * + * A boundary cast (decl-projection.ts in the compiler) checks a value + * package code produced against the type the package's declarations give + * it and builds the native value. Copying would be observable whenever + * something else still references a source object or array, so this pass + * walks the source next to a live capsule of the built value and turns + * every source node with another holder (rc above one) into a live + * capsule of the native value built from it: every holder then reads and + * writes the same native value. Unshared nodes are left to be released + * with their parent, which no program can tell apart from a move. + * + * Phase one checks that every converted node is plain data (the native + * value has no prototype, accessors, symbol keys or frozen state) and + * collects the shared ones; it throws the path TypeError before anything + * changes. Phase two converts the shared nodes in place + * (scr_dyn_decl_become, scr_json.c). */ +#include "scr_runtime.h" + +#include +#include + +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); + +typedef struct { + ScrDyn *node; + ScrDyn *capsule; /* owned */ +} ScrDeclAdopt; + +typedef struct { + ScrDeclAdopt *items; + size_t len, cap; +} ScrDeclAdoptList; + +static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { + if (list->len == list->cap) { + list->cap = list->cap ? list->cap * 2 : 8; + list->items = realloc(list->items, list->cap * sizeof *list->items); + if (!list->items) scr_trap("scriptc: out of memory\n"); + } + list->items[list->len].node = node; + list->items[list->len].capsule = capsule; + list->len++; +} + +static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { + const char *want = NULL; + if (n->kind == SCR_DYN_OBJ) { + if (n->prototype || n->null_proto || n->v.obj.class_name) + want = "a plain object (this value has a prototype)"; + else if (n->symbol_properties || n->symbol_keys) + want = "a plain object (this value has symbol-keyed properties)"; + else if (n->non_extensible) + want = "a plain object (this value is frozen, sealed or not extensible)"; + else if (n->v.obj.source_access && !n->v.obj.source_identity) + want = "a plain object"; + else { + for (size_t i = 0; i < n->v.obj.len && !want; i++) { + const ScrDynEntry *e = &n->v.obj.entries[i]; + if (e->accessor) want = "a plain object (this value has accessor properties)"; + else if (!e->writable || !e->enumerable || !e->configurable) + want = "a plain object (this value has non-default property attributes)"; + } + } + } else if (n->kind == SCR_DYN_ARR) { + if (n->prototype) + want = "a plain array (this value has a prototype)"; + else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) + want = "a plain array (this value has extra properties)"; + else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) + want = "a plain array (this value is frozen, sealed or not extensible)"; + } + if (!want) return true; + scr_dyn_check_fail(path, want, n); + return false; +} + +static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { + for (size_t i = 0; i < list->len; i++) + if (list->items[i].node == node) return list->items[i].capsule; + return NULL; +} + +/* `capsule` is owned by the call: pushed or released. */ +static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, + const ScrDynPath *path, unsigned depth) { + if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || + capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { + scr_dyn_release(capsule); + return true; + } + if (!scr_decl_adopt_plain(n, path)) { + scr_dyn_release(capsule); + return false; + } + if (n->rc > 1 && n->rc != SIZE_MAX) { + ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); + if (seen) { + bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; + scr_dyn_release(capsule); + if (!same) + scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); + return same; + } + scr_decl_adopt_push(list, n, capsule); + } + void *ptr = capsule->v.typed_ref.ptr; + if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { + const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; + for (size_t i = 0; i < ops->count; i++) { + if (!ops->has(ptr, i)) continue; + const ScrStr *name = ops->names[i]; + ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, name->data, 0 }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && + capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; + for (size_t i = 0; i < n->v.arr.len; i++) { + ScrDyn *child = n->v.arr.items[i]; + if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return true; +} + +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { + ScrDeclAdoptList list = { NULL, 0, 0 }; + bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); + for (size_t i = 0; i < list.len; i++) { + ScrDeclAdopt *item = &list.items[i]; + if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_dyn_decl_become(item->node, item->capsule); + scr_dyn_release(item->capsule); + } + free(list.items); +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 987de7758..28c2accce 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1000,11 +1000,6 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); * retained view released with the record. Its capsules adopt that view, and * the typed serializers consult `has` to serialize through the capsule. * `apply` consumes a fresh copying snapshot and returns it merged (+1). */ -/* The declaration-typed boundary's adoption (scr_json.c): every object or - * array of `source` that something else still references becomes a live - * capsule of the native value `capsule` built from it. Throws the path - * TypeError, changing nothing, when a converted node is not plain data. */ -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names, const size_t *lens, size_t count); bool scr_record_source_has(const void *rec); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 336d42e90..dea3426e6 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15756,138 +15756,13 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) { return scr_dyn_object_proto_name(key); } -/* ── declaration-typed boundary: adoption ─────────────────────────────── - * A boundary cast (decl-projection.ts in the compiler) checks a value - * package code produced against the type the package's declarations give - * it and builds the native value. Copying would be observable whenever - * something else still references a source object or array, so this pass - * walks the source next to a live capsule of the built value and turns - * every source node with another holder (rc above one) into a live - * capsule of the native value built from it: every holder then reads and - * writes the same native value. Unshared nodes are left to be released - * with their parent, which no program can tell apart from a move. - * - * Phase one checks that every converted node is plain data (the native - * value has no prototype, accessors, symbol keys or frozen state) and - * collects the shared ones; it throws the path TypeError before anything - * changes. Phase two converts the shared nodes in place. Programs without - * boundary casts never reference this code (the linker drops it). */ -typedef struct { - ScrDyn *node; - ScrDyn *capsule; /* owned */ -} ScrDeclAdopt; - -typedef struct { - ScrDeclAdopt *items; - size_t len, cap; -} ScrDeclAdoptList; - -static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) { - if (list->len == list->cap) { - list->cap = list->cap ? list->cap * 2 : 8; - list->items = realloc(list->items, list->cap * sizeof *list->items); - if (!list->items) scr_trap("scriptc: out of memory\n"); - } - list->items[list->len].node = node; - list->items[list->len].capsule = capsule; - list->len++; -} - -static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) { - const char *want = NULL; - if (n->kind == SCR_DYN_OBJ) { - if (n->prototype || n->null_proto || n->v.obj.class_name) - want = "a plain object (this value has a prototype)"; - else if (n->symbol_properties || n->symbol_keys) - want = "a plain object (this value has symbol-keyed properties)"; - else if (n->non_extensible) - want = "a plain object (this value is frozen, sealed or not extensible)"; - else if (n->v.obj.source_access && !n->v.obj.source_identity) - want = "a plain object"; - else { - for (size_t i = 0; i < n->v.obj.len && !want; i++) { - const ScrDynEntry *e = &n->v.obj.entries[i]; - if (e->accessor) want = "a plain object (this value has accessor properties)"; - else if (!e->writable || !e->enumerable || !e->configurable) - want = "a plain object (this value has non-default property attributes)"; - } - } - } else if (n->kind == SCR_DYN_ARR) { - if (n->prototype) - want = "a plain array (this value has a prototype)"; - else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys) - want = "a plain array (this value has extra properties)"; - else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible) - want = "a plain array (this value is frozen, sealed or not extensible)"; - } - if (!want) return true; - scr_dyn_check_fail(path, want, n); - return false; -} - -static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) { - for (size_t i = 0; i < list->len; i++) - if (list->items[i].node == node) return list->items[i].capsule; - return NULL; -} - -/* `capsule` is owned by the call: pushed or released. */ -static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule, - const ScrDynPath *path, unsigned depth) { - if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule || - capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) { - scr_dyn_release(capsule); - return true; - } - if (!scr_decl_adopt_plain(n, path)) { - scr_dyn_release(capsule); - return false; - } - if (n->rc > 1 && n->rc != SIZE_MAX) { - ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n); - if (seen) { - bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr; - scr_dyn_release(capsule); - if (!same) - scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n); - return same; - } - scr_decl_adopt_push(list, n, capsule); - } - void *ptr = capsule->v.typed_ref.ptr; - if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) { - const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array; - for (size_t i = 0; i < ops->count; i++) { - if (!ops->has(ptr, i)) continue; - const ScrStr *name = ops->names[i]; - ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len); - if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; - ScrDynPath p = { path, name->data, 0 }; - if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) { - if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); - return false; - } - } - } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && - capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { - const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; - for (size_t i = 0; i < n->v.arr.len; i++) { - ScrDyn *child = n->v.arr.items[i]; - if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; - ScrDynPath p = { path, NULL, i }; - if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) { - if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); - return false; - } - } - } - if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); - return true; -} - -/* Weak collections key a plain object by its node and a capsule by its - * native identity: move the node's entries to the identity it now has. */ -static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) { +/* ── declaration-typed boundary: in-place capsules ───────────────────── + * scr_decl_adopt.c (linked only into programs with boundary casts) turns a + * shared source node into a live capsule of the native value built from + * it. The node's old payload moves to a scratch node released here; the + * node's weak-collection entries move to the capsule's native identity. + * Nothing else in the runtime calls this. */ +static bool scr_dyn_decl_rekey(ScrDyn *node, void *ptr) { bool moved = false; if (!scr_weak_observing) return false; for (;;) { @@ -15905,11 +15780,10 @@ static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) { } } -static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) { +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); +void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { ScrDynRegion *region = scr_dyn_region_of(n); if (region) region->intact = false; - /* The old payload moves to a fresh node, which is released below. A - * misc node carries no buffers of an earlier life. */ ScrDyn *old = scr_dyn_alloc(SCR_DYN_NULL); old->kind = n->kind; old->buffer = n->buffer; @@ -15929,36 +15803,14 @@ static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) { n->symbol_properties = NULL; n->symbol_keys = NULL; memset(&n->v, 0, sizeof n->v); + n->v.typed_ref = capsule->v.typed_ref; n->v.typed_ref.ptr = capsule->v.typed_ref.retain(capsule->v.typed_ref.ptr); - n->v.typed_ref.retain = capsule->v.typed_ref.retain; - n->v.typed_ref.release = capsule->v.typed_ref.release; - n->v.typed_ref.type_key = capsule->v.typed_ref.type_key; - n->v.typed_ref.type_key_len = capsule->v.typed_ref.type_key_len; - n->v.typed_ref.materialize = capsule->v.typed_ref.materialize; - n->v.typed_ref.commit = capsule->v.typed_ref.commit; - n->v.typed_ref.array = capsule->v.typed_ref.array; - n->v.typed_ref.record = capsule->v.typed_ref.record; - n->v.typed_ref.klass = capsule->v.typed_ref.klass; - n->v.typed_ref.traced = capsule->v.typed_ref.traced; - n->v.typed_ref.observed = capsule->v.typed_ref.observed; - if (scr_decl_adopt_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; - /* A record with undeclared properties answers them from its retained - * view, as a freshly boxed capsule does. */ - if (scr_record_source_count && n->v.typed_ref.type_key_len >= 7 && - memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { + n->v.typed_ref.materialized = NULL; + n->v.typed_ref.casts = NULL; + if (scr_dyn_decl_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true; + if (n->v.typed_ref.type_key_len >= 7 && memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) { ScrDyn *view = scr_record_source_view(n->v.typed_ref.ptr); if (view) n->v.typed_ref.materialized = scr_dyn_retain(view); } scr_dyn_release(old); } - -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) { - ScrDeclAdoptList list = { NULL, 0, 0 }; - bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0); - for (size_t i = 0; i < list.len; i++) { - ScrDeclAdopt *item = &list.items[i]; - if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_decl_adopt_become(item->node, item->capsule); - scr_dyn_release(item->capsule); - } - free(list.items); -} From 12166956b8e1d3e6fc752be5585832fb72ab700e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:57:29 -0500 Subject: [PATCH 11/30] Spread protocol iterables into Math.min and Math.max Math.min(...src) and Math.max(...src) over an untyped source now take its values the way a spread does: arrays, strings, bytes and collections pack, a native iterator (a Map's keys()) drains, and other iterables (a generator, an object with a Symbol.iterator method) step their iterator to the end. Before, only the packing kinds were accepted and every other iterable threw the spread TypeError. --- packages/runtime/src/scr_json.c | 11 +++++++- tests/corpus/7488-math-spread-untyped.js | 36 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 9abb7796b..c6ff65d59 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1557,6 +1557,8 @@ static ScrDyn *scr_dyn_not_iterable(const ScrDyn *src, const ScrStr *msg) { return NULL; } +static ScrDyn *scr_native_iterator_drain(const ScrDyn *src); + /* Math.max(...src) / Math.min(...src) over an untyped iterable: every * value's ToNumber runs in order, then NaN poisons and ±0 orders as in * Node. A non-iterable source throws V8's call-spread TypeError: `what` @@ -1575,7 +1577,14 @@ double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what) { } static const char spread_msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function"; ScrStr *msg = scr_str_new(spread_msg, sizeof spread_msg - 1); - ScrDyn *pack = scr_dyn_iter_pack(src, msg); + /* The spread takes every value before the call: arrays, strings, bytes + * and collections pack, a native iterator (a Map's keys()) drains, and + * other iterables (a generator, an object with a Symbol.iterator method) + * step their iterator to the end. */ + ScrDyn *pack = src->kind == SCR_DYN_HANDLE && (src->v.handle.tag == SCR_DYNH_ITERATOR || + scr_dyn_handle_ops_of(src)->iter_step) + ? scr_native_iterator_drain(src) + : scr_dyn_destructure_rest(src, msg); scr_str_release(msg); if (!pack) return NAN; double best = max ? -INFINITY : INFINITY; diff --git a/tests/corpus/7488-math-spread-untyped.js b/tests/corpus/7488-math-spread-untyped.js index a30ece320..028135d41 100644 --- a/tests/corpus/7488-math-spread-untyped.js +++ b/tests/corpus/7488-math-spread-untyped.js @@ -31,3 +31,39 @@ for (const k of Object.keys(sources)) { console.log(e.constructor.name, e.message); } } + +// Iterables that step through the iterator protocol, reached through an +// untyped lookup: a generator, an object with a Symbol.iterator method, a +// Set, a Map's keys() and an iterator that ends early. +function* generate(values) { + for (const value of values) yield value; +} +const makers = { + generator: () => generate([4, "1", 7]), + custom: () => ({ + *[Symbol.iterator]() { + yield 10; + yield -2; + }, + }), + set: () => new Set([3, 8, 3]), + keys: () => new Map([[2, "a"], [6, "b"]]).keys(), + empty: () => generate([]), +}; +for (const k of Object.keys(makers)) { + console.log(k, Math.min(...makers[k]()), Math.max(...makers[k]())); +} +const stepped = []; +const counting = { + [Symbol.iterator]() { + let n = 0; + return { + next() { + stepped.push(n); + return n < 3 ? { value: n++, done: false } : { value: undefined, done: true }; + }, + }; + }, +}; +const lookup = { counting }; +console.log(Math.max(...lookup.counting), stepped.join(",")); From 77c063682675520bb5804eb27db6e48bcdc71b80 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:00:01 -0500 Subject: [PATCH 12/30] Include star re-exports in namespace objects, and read causes and instanceof Error like Node under a runtime Error parent - A module namespace converted to an untyped object, and a computed namespace read, use the module's resolved exports, which include the members of `export *` declarations. - `super(message, options)` through a runtime Error parent installs the cause as Error does: an inherited `cause` counts, the value is read through its getter, and the getter's exception propagates. - `value instanceof Error` on an instance of a class whose heritage is evaluated at run time tests that class's prototype chain for Error.prototype. A program with such a class checks prototype chains for `instanceof Error` on untyped values. - The cache of names written on the global object is a Lowerer field instead of a WeakMap keyed by the Lowerer. - errors.test.ts: `"prototype" in Map` answers from the builtin constructor's keys now, so it leaves the refused reads and joins a check of `in` probes against Node's answers. --- .../lowering/class-dynamic-dispatch.ts | 81 +++++++++++++++++++ .../lowering/class-method-mutations.ts | 26 ++++++ .../src/frontend/lowering/dispatch-sites.ts | 1 + .../lowering/expressions/property-access.ts | 6 +- .../compiler/src/frontend/lowering/lowerer.ts | 9 ++- .../lowering/module-namespace-elements.ts | 21 ++--- packages/compiler/src/frontend/ts7/checker.ts | 8 ++ .../main.js.json | 1 + packages/runtime/src/scr_json.c | 13 ++- ...481-class-runtime-heritage-object-error.js | 27 +++++++ .../7487-namespace-spread-untyped/all.js | 2 + .../7487-namespace-spread-untyped/main.js | 5 ++ tests/harness/errors.test.ts | 9 ++- 13 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 tests/corpus/7487-namespace-spread-untyped/all.js diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 8a4a1e417..e394718d1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -908,6 +908,11 @@ export class ClassDynamicDispatch { private readonly prototypeClasses = new Set(); private prototypeIdentityHelper: IrFunction | null = null; private readonly prototypeIdentityClasses = new Set(); + /** `value instanceof Error` through Error.prototype: boxed instances of + * classes with a heritage evaluated at run time test their prototype + * data's chain. */ + private errorPrototypeHelper: IrFunction | null = null; + private readonly errorPrototypeClasses = new Set(); private readonly instancePrototypes = new Map }>(); private readonly checkedCasts = new Map }>(); /** The for-in key helpers, by libCall (dyn and native string[] keys). */ @@ -1114,6 +1119,49 @@ export class ClassDynamicDispatch { }); changed = true; } + if ( + this.errorPrototypeHelper && + !this.errorPrototypeClasses.has(className) && + hasRuntimeHeritage(info) + ) { + this.errorPrototypeClasses.add(className); + const loc = this.errorPrototypeHelper.loc; + const type: IrType = { kind: "object", className }; + const value = varRef("value", DYN, loc); + const prototype = classPrototypeData(lowerer, info, loc, { + kind: "dynCheck", + value, + type, + loc, + }); + if (prototype) + this.errorPrototypeHelper.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [prototype], + type: BOOL, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }); + changed = true; + } if ( this.serializesClasses && !this.jsonPrototypes.has(className) && @@ -1516,6 +1564,31 @@ export class ClassDynamicDispatch { } return { kind: "call", callee: cast.fn.name, args: [expr.value], type, loc: expr.loc }; } + if (expr.kind === "libCall" && expr.fn === "dyn.inheritsErrorPrototype") { + if (!this.errorPrototypeHelper) { + const loc = expr.loc; + this.errorPrototypeHelper = { + name: "%dyn.class.inheritsErrorPrototype", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: BOOL, + loc, + body: [ + { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, + ], + }; + lowerer.liftedFns.push(this.errorPrototypeHelper); + this.generated.add(this.errorPrototypeHelper); + changed = true; + } + return { + kind: "call", + callee: this.errorPrototypeHelper.name, + args: expr.args, + type: BOOL, + loc: expr.loc, + }; + } if ( expr.kind === "libCall" && expr.fn === "dyn.getPrototype" && @@ -5942,3 +6015,11 @@ const INHERITED_OBJECT_METHODS: ReadonlySet = new Set([ "hasOwnProperty", "propertyIsEnumerable", ]); + +/** Whether a class or one of its bases has a heritage evaluated at run + * time, whose prototype chain may reach Error.prototype. */ +function hasRuntimeHeritage(info: ClassInfo): boolean { + for (let current: ClassInfo | null = info; current; current = current.base) + if (current.callableBase) return true; + return false; +} diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index abbaf3a75..fd23b6db8 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -380,6 +380,13 @@ export function collectClassMethodMutations( markTarget(node.initializer, node); if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node)) lowerer.errorPrototypeValues = true; + // A heritage evaluated at run time may be Error, which links the + // class's instances to Error.prototype. + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + runtimeHeritage(lowerer, node) + ) + lowerer.errorPrototypeValues = true; if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { const base = unwrap(node.expression); const prototype = @@ -621,3 +628,22 @@ export function collectClassMethodMutations( if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field); } } + +/** Whether a class's `extends` expression is a value computed at run time: + * anything other than a name of a class declaration or of a builtin + * constructor (`extends Parent` over `const Parent = params.Parent ?? Object`, + * `extends make()`). */ +function runtimeHeritage( + lowerer: Lowerer, + node: ts.ClassDeclaration | ts.ClassExpression, +): boolean { + const heritage = node.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (heritage === undefined) return false; + if (!ts.isIdentifier(heritage) && !ts.isPropertyAccessExpression(heritage)) return true; + let symbol = lowerer.checker.getSymbolAtLocation(heritage); + if (symbol === undefined) return true; + if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol); + return !(symbol.flags & ts.SymbolFlags.Class) && !lowerer.isStdlibSymbol(symbol); +} diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index c8a8d6ba6..32cff333e 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -80,6 +80,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.inheritsErrorPrototype", "dyn.reflectGet", "dyn.reflectSet", ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index d1b9795d5..043ee7d48 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4504,15 +4504,13 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { return count; } -const globalThisWrites = new WeakMap>(); - /** The property names the program assigns on the global object, through * `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set { - const known = globalThisWrites.get(lowerer); + const known = lowerer.globalThisWrittenNames; if (known) return known; const names = new Set(); - globalThisWrites.set(lowerer, names); + lowerer.globalThisWrittenNames = names; for (const sf of lowerer.program.getImplementationSourceFiles()) { if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue; ts.walkPreorder(sf, (node) => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 83ac404d4..c3b555d09 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2969,9 +2969,14 @@ export class Lowerer { * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); /** Whether the program reads `Error.prototype`, the checked-dynamic - * object an ES5 constructor function can inherit from: only then can a - * value that is not an Error instance be `instanceof Error`. */ + * object an ES5 constructor function can inherit from, or has a class + * whose heritage is evaluated at run time (`extends (Base ?? Error)`): + * only then can a value that is not an Error instance be + * `instanceof Error`. */ errorPrototypeValues = false; + /** The property names the program assigns on the global object + * (property-access.ts), collected on first use. */ + globalThisWrittenNames: Set | null = null; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index df40573da..cf828c6f0 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -32,12 +32,13 @@ export function lowerModuleNamespaceElement( try { const body: IrStmt[] = []; const symbol = lowerer.checker.getSymbolAtLocation(source); - symbol?.getExports().forEach((exported, key) => { + for (const exported of symbol ? lowerer.checker.getExportsOfModule(symbol) : []) { + const key = ts.symbolName(exported); const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; - if (!(target.flags & ts.SymbolFlags.Value)) return; + if (!(target.flags & ts.SymbolFlags.Value)) continue; const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; if (!identifier || !ts.isIdentifier(identifier)) { @@ -50,7 +51,7 @@ export function lowerModuleNamespaceElement( kind: "strEq", negated: false, left: varRef("key", STRING, loc), - right: strLit(String(key), loc), + right: strLit(key, loc), type: { kind: "bool" }, loc, }, @@ -58,7 +59,7 @@ export function lowerModuleNamespaceElement( else_: null, loc, }); - }); + } body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); const fn: IrFunction = { name, @@ -111,10 +112,12 @@ export function moduleNamespaceObject( if (!symbol) return null; const exports: { key: string; identifier: ts.Identifier }[] = []; let exact = true; - symbol.getExports().forEach((exported, key) => { + // The module's resolved exports, which include `export *` members. + for (const exported of lowerer.checker.getExportsOfModule(symbol)) { + const key = ts.symbolName(exported); const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; - if (!(target.flags & ts.SymbolFlags.Value)) return; + if (!(target.flags & ts.SymbolFlags.Value)) continue; const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; const immutable = @@ -125,10 +128,10 @@ export function moduleNamespaceObject( (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0)); if (!identifier || !ts.isIdentifier(identifier) || !immutable) { exact = false; - return; + break; } - exports.push({ key: String(key), identifier }); - }); + exports.push({ key, identifier }); + } if (!exact) return null; exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); const name = `%module.object:${ns.type.moduleId}`; diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 9550d1dc2..eb2b9e20b 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -631,6 +631,14 @@ export class CheckerFacade { return aliased; } + /** A module symbol's exports, with the members of its `export *` + * declarations resolved (a symbol's own export table lists only the + * module's own declarations). */ + getExportsOfModule(symbol: Ts7Symbol): readonly Ts7Symbol[] { + this.ensureActive(); + return this.raw.getExportsOfModule(symbol); + } + getDeclaredTypeOfSymbol(symbol: Ts7Symbol): Type { this.ensureActive(); let type = this.cache.declaredTypeOfSymbol.get(symbol); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json index 0c768ef66..b8a66c735 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json @@ -2,6 +2,7 @@ "order": [ "/tests/corpus/7487-namespace-spread-untyped/shapes.js", "/tests/corpus/7487-namespace-spread-untyped/extra.js", + "/tests/corpus/7487-namespace-spread-untyped/all.js", "/tests/corpus/7487-namespace-spread-untyped/main.js" ], "diags": [] diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 57c9e4df4..b0abe20ef 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5146,10 +5146,17 @@ void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *argument scr_dyn_release(value); if (scr_exc_pending()) return; } - if (argc > 1 && argv[1]->kind == SCR_DYN_OBJ && scr_dyn_find_entry(argv[1], "cause", 5)) { - ScrDyn *cause = scr_dyn_obj_get(argv[1], "cause", 5); + /* InstallErrorCause: an options object with a `cause` property, own or + * inherited, defines the value its getter or data property reads. */ + ScrDyn *options = argc > 1 ? argv[1] : NULL; + if (options && options->kind != SCR_DYN_NULL && + (options->kind == SCR_DYN_FUNC || scr_dyn_is_object(options))) { ScrStr *key = scr_str_new("cause", 5); - scr_dyn_define_hidden(receiver, key, cause ? cause : scr_dyn_undefined()); + if (scr_dyn_in(options, key) && !scr_exc_pending()) { + ScrDyn *cause = scr_dyn_obj_read(options, "cause", 5); + if (cause && !scr_exc_pending()) scr_dyn_define_hidden(receiver, key, cause); + scr_dyn_release(cause); + } scr_str_release(key); if (scr_exc_pending()) return; } diff --git a/tests/corpus/7481-class-runtime-heritage-object-error.js b/tests/corpus/7481-class-runtime-heritage-object-error.js index 13d59dc63..32e17c640 100644 --- a/tests/corpus/7481-class-runtime-heritage-object-error.js +++ b/tests/corpus/7481-class-runtime-heritage-object-error.js @@ -22,3 +22,30 @@ try { } catch (err) { console.log(err instanceof E, err.message); } +console.log(e instanceof Error, new A() instanceof Error); +try { + throw new E("caught"); +} catch (err) { + console.log(err instanceof Error, err.message); +} +// The cause option is read like Error's: inherited and accessor causes +// count, and a throwing getter's exception propagates. +const inherited = new E("inherited", Object.create({ cause: "from prototype" })); +console.log(inherited.cause, Object.prototype.hasOwnProperty.call(inherited, "cause")); +let reads = 0; +const accessor = new E("accessor", { + get cause() { + reads++; + return "from getter"; + }, +}); +console.log(accessor.cause, reads, "cause" in new E("none", {}), "cause" in new E("undef", { cause: undefined })); +try { + new E("throws", { + get cause() { + throw new Error("getter failed"); + }, + }); +} catch (err) { + console.log(err.message); +} diff --git a/tests/corpus/7487-namespace-spread-untyped/all.js b/tests/corpus/7487-namespace-spread-untyped/all.js new file mode 100644 index 000000000..e317afdab --- /dev/null +++ b/tests/corpus/7487-namespace-spread-untyped/all.js @@ -0,0 +1,2 @@ +export * from "./shapes.js"; +export const own = "own"; diff --git a/tests/corpus/7487-namespace-spread-untyped/main.js b/tests/corpus/7487-namespace-spread-untyped/main.js index 021c00ac9..2352e844d 100644 --- a/tests/corpus/7487-namespace-spread-untyped/main.js +++ b/tests/corpus/7487-namespace-spread-untyped/main.js @@ -9,3 +9,8 @@ const z = untyped({ ...shapes, ...extra, extra }); console.log(Object.keys(z).join(",")); for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2)); console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size); +// A namespace with star re-exports holds the re-exported members too. +import * as all from "./all.js"; +const w = untyped({ ...all }); +console.log(Object.keys(w).join(","), w.circle(1).area.toFixed(2), w.own, new w.Box(2).size); +for (const key of ["circle", "own", "VERSION", "missing"]) console.log(key, typeof all[key]); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8b0b51b8..d6bee86d6 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1711,9 +1711,10 @@ describe("builtin constructor values (JS lane)", () => { test("members without a lowering stay refused instead of reading undefined", async () => { // Builtin constructors read as interned function values that model only // name, length and constructor. Every other static member or prototype - // read (direct, bracketed, `in`, through an alias or globalThis), and + // read (direct, bracketed, through an alias or globalThis), and // reflection on or redefinition of their members, must stay a located // SC2020/SC1090 refusal, never an undefined or otherwise wrong value. + // `key in Ctor` answers from the constructor's fixed keys. // Test262 lost 769 refusals to undefined reads before this was pinned. const reads = [ ["Map.prototype", "Map.prototype"], @@ -1731,7 +1732,6 @@ describe("builtin constructor values (JS lane)", () => { ], ["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"], ["Map['prototype']", 'Map["prototype"]'], - ["'prototype' in Map", '"prototype" in Map'], ["globalThis.Map.prototype", "globalThis.Map.prototype"], ["Map alias prototype", "MapAlias.prototype"], ["Date alias prototype", "DateAlias.prototype"], @@ -1754,6 +1754,7 @@ console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.lengt console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function); const promisePrototype = Promise.prototype; console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise); +console.log("prototype" in Map, "groupBy" in Map, "captureStackTrace" in Error, "nope" in Map); `; const r = await compileAndRun("builtin-constructor-members", source, "cjs"); expect(r.stderr).toBe(""); @@ -1768,11 +1769,13 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot ), ), ); - // Promise.prototype reads as an object holding the runtime's methods. + // Promise.prototype reads as an object holding the runtime's methods, + // and `in` on a builtin constructor answers from its fixed keys. expect(lines.slice(reads.length)).toEqual([ "7 2 1 1 0 0", "Date Map true true", "object function true", + "true true true false", ]); }); }); From 84eba882764b4f39a44024adc1568b976e8808d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:13:46 -0500 Subject: [PATCH 13/30] Convert typed arrays to BigInt through their element text BigInt(typedArray) compiled in JavaScript but stopped at run time with SC2020. Node converts the typed array through ToPrimitive to its elements joined with "," and parses that text: one element reads as its value, an empty array as 0n, and several elements, fractions, NaN, Infinity or an exponent form throw the SyntaxError for the joined text. - BigInt of a typed array lowers to bigint.parse of its "," join, the same conversion Node performs. - A union whose arms are typed arrays and other BigInt sources dispatches each typed-array arm the same way. Corpus 7403 covers every supported element kind, negative and clamped values, float formatting (2 ** 60 reads through its shortest decimal text), empty arrays and views, the SyntaxError cases and results in arithmetic, and matches Node. Refs #1647 Co-authored-by: Quino Terrasa <26749475+espetro@users.noreply.github.com> --- .../src/frontend/lowering/lower-calls.ts | 15 ++++++ .../7403-bigint-of-typed-arrays.mjs.json | 6 +++ tests/corpus/7403-bigint-of-typed-arrays.mjs | 46 +++++++++++++++++++ .../7403-bigint-of-typed-arrays.mjs.json | 1 + 4 files changed, 68 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json create mode 100644 tests/corpus/7403-bigint-of-typed-arrays.mjs create mode 100644 tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index d18761f98..8424f28c4 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2400,6 +2400,20 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc }; + // A typed array converts through ToPrimitive to its elements joined with + // ",", which BigInt then parses: one element reads as that element, none + // as 0n, and several throw the SyntaxError for the joined text. + if (arg.type.kind === "bytes") { + const text: IrExpr = { + kind: "bytesIntrinsic", + method: "join", + receiver: arg, + args: [{ kind: "strLit", value: ",", type: STRING, loc }], + type: STRING, + loc, + }; + return { kind: "libCall", fn: "bigint.parse", args: [text], type: BIGINT_T, loc }; + } if (arg.type.kind === "bool") { return { kind: "ternary", @@ -2440,6 +2454,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) arm.kind === "string" || arm.kind === "f64" || arm.kind === "bool" || + arm.kind === "bytes" || isUnitType(arm), ) ) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json new file mode 100644 index 000000000..476611adb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7403-bigint-of-typed-arrays.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7403-bigint-of-typed-arrays.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7403-bigint-of-typed-arrays.mjs b/tests/corpus/7403-bigint-of-typed-arrays.mjs new file mode 100644 index 000000000..77ef0cd2e --- /dev/null +++ b/tests/corpus/7403-bigint-of-typed-arrays.mjs @@ -0,0 +1,46 @@ +// BigInt of a typed array converts the array to its elements joined with +// "," and parses that text, as in Node: one element reads as its value, +// an empty array as 0n, and anything else throws the SyntaxError for the +// joined text. Every element kind, negative and clamped values, float +// formatting, views and the results in arithmetic. + +const one = [ + new Int8Array([-5]), + new Uint8Array([7]), + new Uint8ClampedArray([300]), + new Int16Array([-30000]), + new Uint16Array([65535]), + new Int32Array([-2147483648]), + new Uint32Array([4294967295]), + new Float32Array([-0]), + new Float64Array([2 ** 60]), +]; +for (const view of one) console.log(view.constructor.name, BigInt(view)); + +const empty = [new Uint8Array(0), new Float64Array([]), new Int32Array(8).subarray(3, 3)]; +for (const view of empty) console.log("empty", view.constructor.name, BigInt(view)); + +const bad = [ + new Uint8Array([1, 2]), + new Float64Array([1.5]), + new Float32Array([NaN]), + new Float64Array([1e21]), + new Int16Array([3, -4, 5]), + new Float64Array([Infinity]), +]; +for (const view of bad) { + try { + console.log("unexpected", BigInt(view)); + } catch (e) { + console.log(view.constructor.name, e instanceof SyntaxError, e.message); + } +} + +const bytes = new Uint8Array([9, 8, 7]); +const window = bytes.subarray(1, 2); +let total = 0n; +for (let i = 0; i < 100; i++) { + bytes[1] = i; + total += BigInt(window) * 3n; +} +console.log("window", BigInt(window), total, BigInt(bytes.subarray(2)) ** 2n); diff --git a/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json b/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json new file mode 100644 index 000000000..754441b3e --- /dev/null +++ b/tests/harness/corpus-costs/7403-bigint-of-typed-arrays.mjs.json @@ -0,0 +1 @@ +{ "plain": 1738, "san": 3053 } From 143f2e11e9c11dada9f75a75fff4b80978196bd7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:15:57 -0500 Subject: [PATCH 14/30] Keep an own stackTraceLimit on the Error value ahead of the limit --- .../corpus/7603-error-stack-limit-own.js.json | 6 +++++ .../7604-error-stack-limit-frozen.js.json | 6 +++++ packages/runtime/src/scr_json.c | 23 +++++++++++++++++-- tests/corpus/7603-error-stack-limit-own.js | 13 +++++++++++ tests/corpus/7604-error-stack-limit-frozen.js | 9 ++++++++ .../7603-error-stack-limit-own.js.json | 1 + .../7604-error-stack-limit-frozen.js.json | 1 + 7 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json create mode 100644 tests/corpus/7603-error-stack-limit-own.js create mode 100644 tests/corpus/7604-error-stack-limit-frozen.js create mode 100644 tests/harness/corpus-costs/7603-error-stack-limit-own.js.json create mode 100644 tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json new file mode 100644 index 000000000..5ed656fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7603-error-stack-limit-own.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json new file mode 100644 index 000000000..a437ff57e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7604-error-stack-limit-frozen.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 074fe3eae..bc6c9338a 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1878,6 +1878,17 @@ static bool scr_dyn_is_stack_limit(const ScrDyn *d, const char *key, size_t key_ scr_error_ctor_values[0] && d->v.fn.clo == scr_error_ctor_values[0]->v.fn.clo; } +/* An own `stackTraceLimit` on the Error value, assigned a non-number or + * defined with Object.defineProperty, replaces the limit for reads and + * writes through the value; writes to it keep its descriptor. */ +static bool scr_dyn_stack_limit_shadowed(const ScrDyn *d) { + ScrClosure *owner = scr_closure_identity(d->v.fn.clo); + ScrDyn *table = owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL; + bool own = table && scr_dyn_find_entry(table, "stackTraceLimit", 15); + scr_dyn_release(table); + return own; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -9000,12 +9011,19 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; - if (value->kind == SCR_DYN_NUM && scr_dyn_is_stack_limit(recv, key->data, key->len)) { + bool limit = scr_dyn_is_stack_limit(recv, key->data, key->len); + /* A frozen Error keeps the limit read-only. */ + if (limit && value->kind == SCR_DYN_NUM && !scr_dyn_stack_limit_shadowed(recv) && !scr_dyn_is_frozen(recv)) { scr_stack_limit_set(value->v.num); return; } ScrDyn *table = scr_dyn_fn_properties(recv); scr_dyn_object_key_set(table, key, value, recv); + /* A number the own data property takes also sets the limit. */ + ScrDynEntry *own = limit && value->kind == SCR_DYN_NUM && !scr_exc_pending() + ? scr_dyn_find_entry(table, key->data, key->len) + : NULL; + if (own && own->value == value) scr_stack_limit_set(value->v.num); scr_dyn_release(table); return; } @@ -11785,7 +11803,8 @@ static ScrDyn *scr_function_constructor(uint32_t kind) { /* Keyed read on a FUNC node (see scr_runtime.h): own props first, then * the function-instance built-ins name/length. +1 or NULL. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { - if (scr_dyn_is_stack_limit(d, key, key_len)) return scr_dyn_new_num(scr_stack_limit_get()); + if (scr_dyn_is_stack_limit(d, key, key_len) && !scr_dyn_stack_limit_shadowed(d)) + return scr_dyn_new_num(scr_stack_limit_get()); ScrClosure *owner = scr_closure_identity(d->v.fn.clo); if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props) owner->props = scr_box_retain(d->v.fn.class_obj->static_data); diff --git a/tests/corpus/7603-error-stack-limit-own.js b/tests/corpus/7603-error-stack-limit-own.js new file mode 100644 index 000000000..dfd4b7cdf --- /dev/null +++ b/tests/corpus/7603-error-stack-limit-own.js @@ -0,0 +1,13 @@ +// An own stackTraceLimit on the Error constructor held as a value (a +// non-number assigned, or one defined with Object.defineProperty) answers +// reads through the value, and writes to it keep its descriptor. +const E = /** @type {any} */ (Error); +E.stackTraceLimit = "deep"; +console.log(E.stackTraceLimit, typeof E.stackTraceLimit); +E.stackTraceLimit = 2; +console.log(E.stackTraceLimit, Error.stackTraceLimit); +Object.defineProperty(E, "stackTraceLimit", { value: 6, writable: false, configurable: true }); +try { + E.stackTraceLimit = 1; +} catch {} +console.log(E.stackTraceLimit); diff --git a/tests/corpus/7604-error-stack-limit-frozen.js b/tests/corpus/7604-error-stack-limit-frozen.js new file mode 100644 index 000000000..9c73d1403 --- /dev/null +++ b/tests/corpus/7604-error-stack-limit-frozen.js @@ -0,0 +1,9 @@ +// A frozen Error keeps stackTraceLimit read-only through the constructor +// held as a value. +const E = /** @type {any} */ (Error); +E.stackTraceLimit = 3; +Object.freeze(E); +try { + E.stackTraceLimit = 1; +} catch {} +console.log(E.stackTraceLimit, Error.stackTraceLimit); diff --git a/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } diff --git a/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json new file mode 100644 index 000000000..bb380e241 --- /dev/null +++ b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3000 } From 9a452b8129ffe08d16939db5b3614c8279e0553c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:24:18 -0700 Subject: [PATCH 15/30] Keep replaceable method results that have no dynamic form native A method whose name an untyped write can replace is called through the instance's property lookup: a ternary between the dynamic call of an installed replacement and the native call, both boxed into a dynamic value and converted back to the method's result type. A result with no dynamic form, such as an Iterable (its generator arm does not box), stayed unboxed in the dynamic ternary, which failed IR validation with "ternary else-branch: expected dyn, got union". Methods of generic class instantiations always take this path once any write can replace their name, because their prototype data is not modeled, so importing zod (whose JavaScript has unknown-key writes) broke every such call. Such a result now keeps its native type: the native call's value is used as it is, and only a replacement's dynamic result takes the checked conversion into the result type. Results that box are lowered as before. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-callbacks.ts | 72 ++++++++++++------ ...terable-replaceable-method-results.ts.json | 6 ++ ...565-iterable-replaceable-method-results.ts | 76 +++++++++++++++++++ ...terable-replaceable-method-results.ts.json | 1 + 4 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json create mode 100644 tests/corpus/7565-iterable-replaceable-method-results.ts create mode 100644 tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 6ca685684..b51158022 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -6,12 +6,14 @@ import { STRING, VOID, isClassOwnEnumerableFieldName, + typeEquals, type IrExpr, type IrStmt, type SrcLoc, } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; -import type { Lowerer } from "./lowerer.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; import { findMethodOn, type ClassInfo } from "./lower-classes.js"; import { classPropertiesHelper } from "./class-dynamic-dispatch.js"; import { observedPrototypeMethod } from "./class-prototypes.js"; @@ -186,27 +188,53 @@ export function classCallbackCall( loc, }; const observed = observedSlot(lowerer, receiver, name, loc); - const result: IrExpr = observed - ? call - : { - kind: "ternary", - cond: varRef(present.id, BOOL, loc), - then: call, - else_: - fallback.type.kind === "void" - ? { - kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: fallback, loc }], - result: dynUndefinedExpr(loc), - type: DYN, - loc, - } - : lowerer.coerceToExpected(fallback, DYN), - type: DYN, - loc, - }; - const answer = - fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + const boxed = + observed || fallback.type.kind === "void" ? null : lowerer.coerceToExpected(fallback, DYN); + let answer: IrExpr; + if (boxed !== null && boxed.type.kind !== "dyn") { + // A result with no checked-dynamic form (an Iterable, whose + // generator arm does not box) keeps its native type: the unchanged + // method's result stays as it is, and a replacement's dynamic result + // takes the checked conversion into that type. + const converted = lowerer.coerceToExpected(call, fallback.type); + if (!typeEquals(converted.type, fallback.type)) { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + loc, + `calling replaceable method '${calleeName}' with a '${lowerer.fmt(fallback.type)}' result`, + ), + ); + throw new PoisonError(); + } + answer = { + kind: "ternary", + cond: varRef(present.id, BOOL, loc), + then: converted, + else_: fallback, + type: fallback.type, + loc, + }; + } else { + const result: IrExpr = observed + ? call + : { + kind: "ternary", + cond: varRef(present.id, BOOL, loc), + then: call, + else_: boxed ?? { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: fallback, loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + type: DYN, + loc, + }; + answer = + fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + } return { kind: "seqExpr", stmts: [ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json new file mode 100644 index 000000000..60631a82f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7565-iterable-replaceable-method-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7565-iterable-replaceable-method-results.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7565-iterable-replaceable-method-results.ts b/tests/corpus/7565-iterable-replaceable-method-results.ts new file mode 100644 index 000000000..4e71a0dcc --- /dev/null +++ b/tests/corpus/7565-iterable-replaceable-method-results.ts @@ -0,0 +1,76 @@ +// A method returning Iterable whose name an untyped write can replace +// (`holder.candidates = ...` through `any`) is called through the +// instance's property lookup: the native method runs while the slot is +// unchanged, and a replacement installed on an instance runs instead, +// its result iterated as an Iterable. + +interface Lookup { + readonly index: string; + readonly key: string; +} +interface Query { + readonly lookups?: readonly Lookup[]; +} +interface Row { + id: string; + n: number; +} + +class Repo { + private readonly rows = new Map(); + add(row: T): void { + this.rows.set(row.id, row); + } + candidates(lookups: readonly Lookup[]): Iterable { + if (lookups.length === 0) return this.rows.values(); + const out: T[] = []; + for (const row of this.rows.values()) if (lookups.every((l) => l.key === row.id)) out.push(row); + return out; + } + query(query: Query): T[] { + const matches: T[] = []; + for (const row of this.candidates(query.lookups ?? [])) matches.push(row); + return matches; + } + ids(query: Query): string { + let out = ""; + for (const row of this.candidates(query.lookups ?? [])) out += (out ? "," : "") + row.id; + return out; + } +} + +function seed(): Repo { + const r = new Repo(); + r.add({ id: "a", n: 1 }); + r.add({ id: "b", n: 2 }); + r.add({ id: "c", n: 3 }); + return r; +} + +const r = seed(); +console.log(r.query({}).length, r.query({ lookups: [{ index: "i", key: "b" }] })); +console.log(r.ids({}), r.ids({ lookups: [{ index: "i", key: "c" }] })); + +// An untyped write of the same name: on an unrelated object first, then +// on one Repo instance, whose own property shadows the class method. +const holder: any = {}; +holder.candidates = (): Row[] => []; +console.log(typeof holder.candidates); + +const replaced = seed(); +const untyped: any = replaced; +untyped.candidates = function (lookups: readonly Lookup[]): Row[] { + return [{ id: "z" + lookups.length, n: 9 }]; +}; +console.log(replaced.query({}), replaced.query({ lookups: [{ index: "i", key: "a" }] })); +console.log(replaced.ids({})); + +// A generator replacement is single-pass, as in JavaScript. +untyped.candidates = function* (): Generator { + yield { id: "g1", n: 10 }; + yield { id: "g2", n: 11 }; +}; +console.log(replaced.query({}).map((row) => row.id + ":" + row.n)); + +// The untouched instance still runs the class method. +console.log(r.ids({})); diff --git a/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json b/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json new file mode 100644 index 000000000..6d81df217 --- /dev/null +++ b/tests/harness/corpus-costs/7565-iterable-replaceable-method-results.ts.json @@ -0,0 +1 @@ +{ "plain": 8000, "san": 14000 } From f030e5ad44418e398dbfc395a054d3ae04bd3a28 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:36:11 -0500 Subject: [PATCH 16/30] Check with tsc-ts 0.0.8 on every platform tsc-ts 0.0.8's API server serves on the named pipe the Node host connects over on Windows (tsc-ts --api --pipe), and the native compiler already starts it over its standard streams there, so tsc-ts is the default checker on every platform. SCRIPTC_CHECKER=tsgo still selects tsgo. The compiler depends on tsc-ts 0.0.8, which the release-age exclusions admit with its platform packages and @scriptc/threads 0.4.2. The Windows CLI smoke test runs with both checkers, and verify-native-cli.mjs --run checks that a distribution's tsc-ts runs on its platform (tsc-ts.exe on Windows). --- .github/workflows/ci.yml | 19 +++-- docs/content/docs/cli.mdx | 2 +- packages/compiler/package.json | 2 +- .../src/frontend/ts7/checker-backend.test.ts | 14 ++-- .../src/frontend/ts7/checker-backend.ts | 12 +-- .../compiler/src/native/toolchain.test.ts | 6 +- packages/compiler/src/native/toolchain.ts | 8 +- pnpm-lock.yaml | 74 +++++++++---------- pnpm-workspace.yaml | 18 ++--- scripts/verify-native-cli.mjs | 10 +++ 10 files changed, 86 insertions(+), 79 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a100e374..f131747fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -773,17 +773,22 @@ jobs: run: >- pnpm exec vitest run tests/harness/worktree-files.test.ts --testNamePattern "workspace reset keeps Linux Sandbox paths POSIX" + # The Node host checks with tsc-ts, the default, over the named pipe it + # serves on Windows, and with tsgo. - name: Windows scriptc run smoke shell: pwsh run: | zig version - $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $actual = ($output -join "`n").Trim() - if ($actual -ne "hello world") { - throw "unexpected scriptc output: $actual" + foreach ($checker in @("tsc-ts", "tsgo")) { + $env:SCRIPTC_CHECKER = $checker + $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $actual = ($output -join "`n").Trim() + if ($actual -ne "hello world") { + throw "unexpected scriptc output with ${checker}: $actual" + } } - name: Windows PE subsystem selection env: diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 8c57d2e2a..8b0353620 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -206,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
A runtime pack directory to use instead of an installed @scriptc/runtime-<target> package.
SCRIPTC_CHECKER
-
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is tsgo, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.
+
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
```console diff --git a/packages/compiler/package.json b/packages/compiler/package.json index a1d66963d..a358ae545 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.7", + "tsc-ts": "0.0.8", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/frontend/ts7/checker-backend.test.ts b/packages/compiler/src/frontend/ts7/checker-backend.test.ts index e37357afa..cd37bafff 100644 --- a/packages/compiler/src/frontend/ts7/checker-backend.test.ts +++ b/packages/compiler/src/frontend/ts7/checker-backend.test.ts @@ -24,15 +24,11 @@ function writePackage(directory: string, name: string, executable?: string): voi writeFileSync(join(directory, "lib", executable), ""); } -test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => { - expect(checkerBackend({}, "linux")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts"); - expect(checkerBackend({}, "darwin")).toBe("tsc-ts"); - expect(checkerBackend({}, "win32")).toBe("tsgo"); - expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts"); - expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo"); +test("tsc-ts is the default checker, and SCRIPTC_CHECKER selects one", () => { + expect(checkerBackend({})).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "" })).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" })).toBe("tsc-ts"); + expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" })).toBe("tsgo"); expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow( 'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"', ); diff --git a/packages/compiler/src/frontend/ts7/checker-backend.ts b/packages/compiler/src/frontend/ts7/checker-backend.ts index 14418cc94..0b32f24f6 100644 --- a/packages/compiler/src/frontend/ts7/checker-backend.ts +++ b/packages/compiler/src/frontend/ts7/checker-backend.ts @@ -4,17 +4,13 @@ import { fileURLToPath } from "node:url"; /** The TypeScript 7 API servers a compile can check with: tsc-ts, the default, * and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`. - * Both serve the same API and give the same results. On Windows the default is - * tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's - * API server has no named-pipe transport yet. */ + * Both serve the same API and give the same results. On Windows the Node host + * connects to either over a named pipe the server creates (rpc-process.ts). */ export type CheckerBackend = "tsgo" | "tsc-ts"; -export function checkerBackend( - env: NodeJS.ProcessEnv = process.env, - platform: string = process.platform, -): CheckerBackend { +export function checkerBackend(env: NodeJS.ProcessEnv = process.env): CheckerBackend { const value = env["SCRIPTC_CHECKER"]; - if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts"; + if (value === undefined || value === "") return "tsc-ts"; if (value === "tsc-ts") return "tsc-ts"; if (value === "tsgo") return "tsgo"; throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`); diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts index 0ee434300..ad4570c0c 100644 --- a/packages/compiler/src/native/toolchain.test.ts +++ b/packages/compiler/src/native/toolchain.test.ts @@ -99,11 +99,11 @@ test("native toolchain assets resolve from the physical manifest through package const direct = loadNativeToolchain(path); const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json")); expect(viaLink).toEqual(direct); - expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe")); + expect(viaLink.ts7Executable).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); expect( - loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" }) + loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsgo" }) .ts7Executable, - ).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe")); + ).toBe(join(assets, "typescript/lib/tsc.exe")); expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe")); expect( loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts index fbcd94251..5abbb329e 100644 --- a/packages/compiler/src/native/toolchain.ts +++ b/packages/compiler/src/native/toolchain.ts @@ -177,11 +177,11 @@ export function loadNativeToolchain( : [...target.defaultLinkerArgs]; return { compilerVersion: manifest.compiler_version, - // tsc-ts, the default checker except on Windows, comes with the distribution; - // a development build takes it from the tsc-ts package installed with the - // compiler or for the working directory. SCRIPTC_CHECKER selects a checker. + // tsc-ts, the default checker, comes with the distribution; a development + // build takes it from the tsc-ts package installed with the compiler or for + // the working directory. SCRIPTC_CHECKER selects a checker. ts7Executable: - checkerBackend(env, host.platform) === "tsgo" + checkerBackend(env) === "tsgo" ? pathFrom(root, manifest.ts7) : manifest.tsc_ts !== undefined ? pathFrom(root, manifest.tsc_ts) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 589de2a51..525994ba1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,8 +110,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.8 + version: 0.0.8 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1360,8 +1360,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.4.1': - resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==} + '@scriptc/threads@0.4.2': + resolution: {integrity: sha512-nR9FUxpOpgtTmJu6dejttElBL+JJTAZzJ2DZM+eNoujmLx9VpdlAVdgj3cu/MDsTUg/1sIUBq1ME1csC/Aa6yg==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1377,42 +1377,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.7': - resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==} + '@tsc-ts/darwin-arm64@0.0.8': + resolution: {integrity: sha512-+hB9yWk58lwZHGrVEzqEC4D7MQW7Vd2z7blV3TULIKZPTMwdf8aT7XFAmfP48PoGff0H9yuQKyRXJl9I+sQv9w==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.7': - resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==} + '@tsc-ts/darwin-x64@0.0.8': + resolution: {integrity: sha512-yz4UddicqMXe+T7nV7ftpf155lkiK6gIPNwSB8rH9dlwiV6a17CO/vpSk627QLMqnfGNicB6YwLMk/SJ9moPdw==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.7': - resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==} + '@tsc-ts/linux-arm64-gnu@0.0.8': + resolution: {integrity: sha512-LHqfT9mQ4tusPdWmnanCvC8/YBOF5bHM5h5863a3t2HTgFVdps1Qwb0yzrW5DDbojIhQ0Z+GupxtXVUQcV0Q/Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.7': - resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==} + '@tsc-ts/linux-arm64-musl@0.0.8': + resolution: {integrity: sha512-I0CC3xZMwoQM6OiVsubRHb0VI/RrJpneb33kDQhadYHq3taPos71KwP+CbCXNu88rNsDsyn7D1oCVfJb73g4Mw==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.7': - resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==} + '@tsc-ts/linux-x64-gnu@0.0.8': + resolution: {integrity: sha512-PLERuwUuxT92x0BZAMQubiQP5jhlqoLmswaZWqcvfKAZyBp0TbWiVOmuNnDbJozvBaNUrfdGPb9suiQ+Me7M6A==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.7': - resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==} + '@tsc-ts/linux-x64-musl@0.0.8': + resolution: {integrity: sha512-zDl36l1uyWc0mQniOAMZmkVD9TrlFExHlDT0fL8JGD4XLgx+bRe+To38yP0KSBehXBzjDNn8xrEoJ+3RP6mYwQ==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.7': - resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==} + '@tsc-ts/win32-x64-msvc@0.0.8': + resolution: {integrity: sha512-9gO+pFoLiIxITlAE2P6JL8ihZwns7z/2Raw5+38RqLzHmnSsRN47+H4p/4EqQsLW5/TTsAo9uSeSkf3EuRg+XA==} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.7: - resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==} + tsc-ts@0.0.8: + resolution: {integrity: sha512-EiyDziYOryBg/YEvev/mKCn+hD4M6VFjH7VP9nv2WadDKYaPrdb+a3hP+wYr/UsDWNE7AdL7YGnjZppeXlXy6g==} engines: {node: '>=20'} hasBin: true @@ -3439,7 +3439,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.4.1': {} + '@scriptc/threads@0.4.2': {} '@sinclair/typebox@0.25.24': {} @@ -3454,25 +3454,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.7': + '@tsc-ts/darwin-arm64@0.0.8': optional: true - '@tsc-ts/darwin-x64@0.0.7': + '@tsc-ts/darwin-x64@0.0.8': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.7': + '@tsc-ts/linux-arm64-gnu@0.0.8': optional: true - '@tsc-ts/linux-arm64-musl@0.0.7': + '@tsc-ts/linux-arm64-musl@0.0.8': optional: true - '@tsc-ts/linux-x64-gnu@0.0.7': + '@tsc-ts/linux-x64-gnu@0.0.8': optional: true - '@tsc-ts/linux-x64-musl@0.0.7': + '@tsc-ts/linux-x64-musl@0.0.8': optional: true - '@tsc-ts/win32-x64-msvc@0.0.7': + '@tsc-ts/win32-x64-msvc@0.0.8': optional: true '@tybys/wasm-util@0.10.3': @@ -4813,17 +4813,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.7: + tsc-ts@0.0.8: dependencies: - '@scriptc/threads': 0.4.1 + '@scriptc/threads': 0.4.2 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.7 - '@tsc-ts/darwin-x64': 0.0.7 - '@tsc-ts/linux-arm64-gnu': 0.0.7 - '@tsc-ts/linux-arm64-musl': 0.0.7 - '@tsc-ts/linux-x64-gnu': 0.0.7 - '@tsc-ts/linux-x64-musl': 0.0.7 - '@tsc-ts/win32-x64-msvc': 0.0.7 + '@tsc-ts/darwin-arm64': 0.0.8 + '@tsc-ts/darwin-x64': 0.0.8 + '@tsc-ts/linux-arm64-gnu': 0.0.8 + '@tsc-ts/linux-arm64-musl': 0.0.8 + '@tsc-ts/linux-x64-gnu': 0.0.8 + '@tsc-ts/linux-x64-musl': 0.0.8 + '@tsc-ts/win32-x64-msvc': 0.0.8 tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ceb1506e9..e74d40442 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,15 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.7" - - "@tsc-ts/darwin-x64@0.0.7" - - "@tsc-ts/linux-arm64-gnu@0.0.7" - - "@tsc-ts/linux-arm64-musl@0.0.7" - - "@tsc-ts/linux-x64-gnu@0.0.7" - - "@tsc-ts/linux-x64-musl@0.0.7" - - "@tsc-ts/win32-x64-msvc@0.0.7" - - "tsc-ts@0.0.7" - - "@scriptc/threads@0.4.1" + - "@tsc-ts/darwin-arm64@0.0.8" + - "@tsc-ts/darwin-x64@0.0.8" + - "@tsc-ts/linux-arm64-gnu@0.0.8" + - "@tsc-ts/linux-arm64-musl@0.0.8" + - "@tsc-ts/linux-x64-gnu@0.0.8" + - "@tsc-ts/linux-x64-musl@0.0.8" + - "@tsc-ts/win32-x64-msvc@0.0.8" + - "tsc-ts@0.0.8" + - "@scriptc/threads@0.4.2" allowBuilds: esbuild: true overrides: diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs index 115573b5c..92de57938 100644 --- a/scripts/verify-native-cli.mjs +++ b/scripts/verify-native-cli.mjs @@ -94,5 +94,15 @@ if (process.argv.includes("--run")) { }).trim(); if (version !== identity.version) throw new Error(`native command reported ${version}, expected ${identity.version}`); + // tsc-ts, the default checker, runs on the distribution's own platform. + const tscTs = resolve(dirname(binary), hostPath(toolchain.tsc_ts)); + if (windows && !tscTs.endsWith("tsc-ts.exe")) + throw new Error(`tsc_ts is not tsc-ts.exe: ${tscTs}`); + const tscTsVersion = execFileSync(tscTs, ["--version"], { + encoding: "utf8", + env: { ...process.env, PATH: "" }, + }).trim(); + if (!tscTsVersion.startsWith("Version ")) + throw new Error(`the distribution's tsc-ts reported ${JSON.stringify(tscTsVersion)}`); } console.log(`${identity.name}@${identity.version}: native distribution verified`); From 1e35b1c56c46d743ed7c4fb010b844d0222572b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:36:34 -0500 Subject: [PATCH 17/30] Build the native compiler again The self-hosted compiler spreads only known record shapes and tests a value against null with == or != only as a plain read (SC1090). rewriteCandidates in int32-arrays.ts spread whole IR statement and expression unions: each spread now names one kind. The loop and block cases spread per kind, the hoisted read and sequence rewrites narrow with an if, switch cases spread through a typed helper, and the inlined ternary, a fresh copy, takes the call's location directly. The checked int32 array test in int32-slots.ts binds the range it reads first. --- packages/compiler/src/ir/int32-arrays.ts | 21 +++++++++++++++++---- packages/compiler/src/ir/int32-slots.ts | 5 ++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index bdb3f21e2..02de7268f 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -1,4 +1,6 @@ import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; + +type SwitchCase = Extract["cases"][number]; import { reparseExpr } from "./serialize.js"; import { everyExprChild, @@ -382,20 +384,26 @@ function rewriteCandidates( if (!call || simple(call.args[1]!)) return [s]; const { decl, ref } = bind(call.args[1]!); const read: IrExpr = { ...call, args: [call.args[0]!, ref] }; - return [decl, s.kind === "varDecl" ? { ...s, init: read } : { ...s, value: read }]; + if (s.kind === "varDecl") return [decl, { ...s, init: read }]; + return [decl, { ...s, value: read }]; }); + const hoistCase = (c: SwitchCase): SwitchCase => ({ ...c, body: hoist(c.body) }); const lists = (s: IrStmt): IrStmt => { switch (s.kind) { case "if": return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) }; case "block": + return { ...s, body: hoist(s.body) }; case "while": + return { ...s, body: hoist(s.body) }; case "doWhile": + return { ...s, body: hoist(s.body) }; case "for": + return { ...s, body: hoist(s.body) }; case "forOf": return { ...s, body: hoist(s.body) }; case "switch": - return { ...s, cases: s.cases.map((c) => ({ ...c, body: hoist(c.body) })) }; + return { ...s, cases: s.cases.map((c) => hoistCase(c)) }; case "tryCatch": return { ...s, @@ -425,7 +433,10 @@ function rewriteCandidates( }; } const call = readerCall(e); - if (!call) return e.kind === "seqExpr" ? { ...e, stmts: hoist(e.stmts) } : e; + if (!call) { + if (e.kind === "seqExpr") return { ...e, stmts: hoist(e.stmts) }; + return e; + } const helper = readers.get(call.callee)!; const array = call.args[0]!; const [arrayParam, indexParam] = helper.params; @@ -448,7 +459,9 @@ function rewriteCandidates( }, stmt: (x) => x, }); - const typed: IrExpr = { ...body, loc: e.loc }; + // The ternary is a fresh copy (reparseExpr): it takes the call's location. + const typed = body; + typed.loc = e.loc; return stmts.length === 0 ? typed : { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc }; diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index e6491ba80..4a2abc5b0 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -1131,7 +1131,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): const key = `l:${fn.name}\0${id}`; if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; slots.dropInt32Elements(key); - const checkable = (value: IrExpr): boolean => int32(value) || ranges.get(value) != null; + const checkable = (value: IrExpr): boolean => { + const range = ranges.get(value); + return int32(value) || range != null; + }; if (values.every(checkable) && checkableInt32Allocations(fn, id)) slots.checkedInt32Arrays.add(`${fn.name}\0${id}`); else reason(`array:${fn.name}\0${id}`, "an element store is not int32"); From 0b969479c3a66c0974892476ad69127ba74817fe Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 18:21:31 -0700 Subject: [PATCH 18/30] Build fresh literals a JavaScript callee may reshape as plain objects A fresh TypeScript data literal passed to a checked-dynamic slot became a typed view whose record layout requires every property, so a JavaScript callee deleting one (zod 4's `normalizeParams` deletes `params.message` from `.refine(fn, { message })`) threw "Cannot delete property". When the callee is implemented in JavaScript (a call through an untyped value, an untyped parameter of a JavaScript function, or a non-library `.d.ts` signature whose parameter is untyped or makes one of the literal's properties optional), the literal has no typed alias and is now built as the plain object the callee receives. Standard-library and TypeScript callees, and literals with methods or accessors, keep their native storage. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 48 ++++++++++++++++++- .../main.ts.json | 7 +++ .../7603-fresh-literal-untyped-param/lib.js | 12 +++++ .../7603-fresh-literal-untyped-param/main.ts | 18 +++++++ .../node_modules/params-kit/lib/index.d.ts | 10 ++++ .../node_modules/params-kit/lib/index.js | 11 +++++ .../node_modules/params-kit/package.json | 1 + .../main.ts.json | 1 + 8 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/lib.js create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/main.ts create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js create mode 100644 tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json create mode 100644 tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 93c6246d9..47db82f05 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2405,6 +2405,51 @@ export class Lowerer { return fallback ?? null; } + /** A fresh TypeScript data literal (`{ message: "m" }`) passed to a + * checked-dynamic parameter of a callee implemented in JavaScript, whose + * contract lets it reshape the object: a call through an untyped value, an + * untyped parameter of a JavaScript function, or a package-declared + * parameter type (a non-library `.d.ts`) that makes one of the literal's + * properties optional (zod's `normalizeParams` deletes `params.message`). + * No typed alias of a fresh literal exists, so it is built as the plain + * object the callee receives instead of a view whose record layout refuses + * that delete. Standard-library and TypeScript callees, and literals with + * methods or accessors, keep their native storage. */ + private reshapeableContextualLiteral( + literal: ts.ObjectLiteralExpression, + contextual: readonly ts.Type[], + ): boolean { + const call = literal.parent; + if ( + call === undefined || + !(ts.isCallExpression(call) || ts.isNewExpression(call)) || + !call.arguments?.nodes.includes(literal) || + literal.properties.nodes.length === 0 || + !literal.properties.nodes.every( + (prop) => + (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && + (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)), + ) + ) + return false; + const declaration = this.checker.getResolvedSignature(call)?.declaration; + const file = declaration ? ts.getSourceFileOfNode(declaration) : undefined; + const untyped = contextual.some((part) => part.flags & ts.TypeFlags.Any); + if (file === undefined) return untyped; + if (isJsSourceFile(file)) return untyped; + if (!file.isDeclarationFile || this.isStdlibFile(file)) return false; + if (untyped) return true; + const objects = contextual.filter((part) => part.flags & ts.TypeFlags.Object); + if (objects.length === 0) return false; + return literal.properties.nodes.some((prop) => { + const name = (prop.name as ts.Identifier | ts.StringLiteral).text; + return objects.every((part) => { + const symbol = this.checker.getPropertyOfType(part, name); + return symbol !== undefined && (symbol.flags & ts.SymbolFlags.Optional) !== 0; + }); + }); + } + /** A TypeScript object literal handed to a JavaScript checked-value * parameter whose inferred type is open (`constructor({ factory, ...options * } = {})` — the rest element admits any property): the literal names a @@ -10652,7 +10697,8 @@ export class Lowerer { builtinOption("UrlObject") || builtinOption("URLFormatOptions") || !this.mapTypeOf(this.typeOf(x)) || - this.excessJsContextualProperty(x, parts) + this.excessJsContextualProperty(x, parts) || + this.reshapeableContextualLiteral(x, parts) ) { return lowerDynObjectLiteral(this, x); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json new file mode 100644 index 000000000..5b1ecde5e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-fresh-literal-untyped-param/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7603-fresh-literal-untyped-param/lib.js", + "/tests/corpus/7603-fresh-literal-untyped-param/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7603-fresh-literal-untyped-param/lib.js b/tests/corpus/7603-fresh-literal-untyped-param/lib.js new file mode 100644 index 000000000..687caf5c2 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/lib.js @@ -0,0 +1,12 @@ +// zod 4's normalizeParams: moves `message` to `error` and deletes it. +export function normalize(params) { + if (params?.message !== undefined) params.error = params.message; + delete params.message; + return params; +} + +export function strip(options) { + for (const key of Object.keys(options)) if (options[key] === undefined) delete options[key]; + options.stripped = true; + return options; +} diff --git a/tests/corpus/7603-fresh-literal-untyped-param/main.ts b/tests/corpus/7603-fresh-literal-untyped-param/main.ts new file mode 100644 index 000000000..c242c7150 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/main.ts @@ -0,0 +1,18 @@ +// A fresh TypeScript object literal handed to an untyped JavaScript +// parameter, or to a parameter type that declares its properties optional, +// is the plain object the callee may reshape: deleting a property works as +// in Node instead of failing on a record layout that requires it. +import { normalize, strip } from "./lib.js"; + +console.log(JSON.stringify(normalize({ message: "must change at least one field" }))); +console.log(JSON.stringify(normalize({ message: "kept", note: 1 }))); +const n = 2; +console.log(JSON.stringify(strip({ a: n, b: undefined, c: "x" }))); + + +// A package-declared parameter whose type makes `message` optional, on a +// method the package's JavaScript defines (zod's `.refine(fn, { message })`). +import { schema } from "params-kit"; +const s = schema("book"); +console.log(s.refine("plain", { message: "must change at least one field" })); +console.log(s.refine("with path", { message: "bad", path: ["a", "b"] })); diff --git a/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts new file mode 100644 index 000000000..608534249 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.d.ts @@ -0,0 +1,10 @@ +export interface RefineParams { + message?: string; + error?: unknown; + path?: string[]; +} +export interface Schema { + name: string; + refine(label: string, params?: string | RefineParams): string; +} +export declare function schema(name: string): Schema; diff --git a/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js new file mode 100644 index 000000000..2433d9129 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/lib/index.js @@ -0,0 +1,11 @@ +// zod 4's normalizeParams on an instance method, behind a package declaration. +function normalizeParams(params) { + if (params?.message !== undefined) params.error = params.message; + delete params.message; + return params; +} +export function schema(name) { + const inst = { name }; + inst.refine = (label, params) => `${inst.name}.${label}: ${JSON.stringify(normalizeParams(params))}`; + return inst; +} diff --git a/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json new file mode 100644 index 000000000..1addbb8d0 --- /dev/null +++ b/tests/corpus/7603-fresh-literal-untyped-param/node_modules/params-kit/package.json @@ -0,0 +1 @@ +{ "name": "params-kit", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json b/tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json new file mode 100644 index 000000000..62c164e5c --- /dev/null +++ b/tests/harness/corpus-costs/7603-fresh-literal-untyped-param/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 3200 } From 1e4889bebeee87710ff37aa565272bc0369d41c6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:44:30 -0500 Subject: [PATCH 19/30] Declare the stream write-with-encoding helper's IrExpr result --- packages/compiler/src/frontend/lowering/lower-stream.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index e35f5564f..c37ae9074 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -5683,7 +5683,10 @@ export function lowerStreamMethodCall( // argument reaches an object-mode side's _write as passed, null when // the callback took its place (a byte-mode side encodes a string // chunk with it). - const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { + const writeEnc = ( + encArg: ts.Expression | undefined, + cbArg: ts.Expression | undefined, + ): IrExpr => { const receiver = streamReceiverExpr(lowerer, access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); const tail: IrExpr[] = []; @@ -5696,7 +5699,7 @@ export function lowerStreamMethodCall( args: [receiver, numLit(flags, loc), chunk, ...tail], type: BOOL, loc, - } satisfies IrExpr; + }; }; if (kind === "dyn") { if (args.length === 3) return writeEnc(args[1], args[2]); From dd770477b0960ddfb21ec6cd94509950836bb540 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:47:13 -0700 Subject: [PATCH 20/30] Format the boundary harness test Co-Authored-By: Claude Opus 5.5 --- tests/harness/decl-boundary.test.ts | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts index 7f8e93c84..6729dd530 100644 --- a/tests/harness/decl-boundary.test.ts +++ b/tests/harness/decl-boundary.test.ts @@ -17,17 +17,21 @@ const entry = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/lying.ts"); const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/decl-boundary"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; -test("boundary casts throw a path TypeError for values the declarations misdescribe", { timeout: 600_000 }, async () => { - mkdirSync(outDir, { recursive: true }); - const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); - if (!result.ok) { - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - } - const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); - pending.child.stdin?.end(); - const { stdout } = await pending; - expect(stdout).toBe( - "liar threw true expected number at $.n, got string\n" + - "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", - ); -}); +test( + "boundary casts throw a path TypeError for values the declarations misdescribe", + { timeout: 600_000 }, + async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "lying"), outDir, sanitize }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" }); + pending.child.stdin?.end(); + const { stdout } = await pending; + expect(stdout).toBe( + "liar threw true expected number at $.n, got string\n" + + "frozen threw expected a plain object (this value is frozen, sealed or not extensible) at $, got object\n", + ); + }, +); From e3e955eda084a4599f95f69eba34c2d264d1bd1b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:55:26 -0500 Subject: [PATCH 21/30] Register Math.min and Math.max spreads as generator steps dyn.mathFold steps the iterator of an untyped iterable, so the generator dispatch pass now counts it among the runtime entry points that step iterators. A generator whose only consumer is Math.min or Math.max then has its step hooks registered, and the spread folds its values instead of throwing "Reading this native iterator result has no lowering". --- packages/compiler/src/frontend/lowering/dispatch-sites.ts | 5 +++-- .../tests/corpus/7489-math-spread-generator-only.js.json | 6 ++++++ tests/corpus/7489-math-spread-generator-only.js | 8 ++++++++ .../corpus-costs/7489-math-spread-generator-only.js.json | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json create mode 100644 tests/corpus/7489-math-spread-generator-only.js create mode 100644 tests/harness/corpus-costs/7489-math-spread-generator-only.js.json diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index c8a8d6ba6..f31e5a6ae 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -12,10 +12,11 @@ import { toPrimitiveHint } from "./class-dynamic-dispatch.js"; import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js"; /** Runtime entry points that step a checked-dynamic iterable's iterator: - * destructuring rest, and the collection constructors' and - * Object.fromEntries' seeds. */ + * destructuring rest, the collection constructors' and Object.fromEntries' + * seeds, and Math.min/Math.max over a spread. */ const RUNTIME_STEP_LIB_FNS: ReadonlySet = new Set([ "dyn.destructureRest", + "dyn.mathFold", "dyn.nativeSetNew", "dyn.mapSeedEntries", "dyn.fromEntries", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json new file mode 100644 index 000000000..5277102eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7489-math-spread-generator-only.js" + ], + "diags": [] +} diff --git a/tests/corpus/7489-math-spread-generator-only.js b/tests/corpus/7489-math-spread-generator-only.js new file mode 100644 index 000000000..168057aac --- /dev/null +++ b/tests/corpus/7489-math-spread-generator-only.js @@ -0,0 +1,8 @@ +// Math.max and Math.min as the only consumers of an untyped generator: the +// spread steps the generator, whose values fold with ToNumber as in Node. +function* generate(limit) { + for (let i = 1; i <= limit; i++) yield i % 2 === 0 ? String(i * 3) : i; +} +const lookup = { generate }; +console.log(Math.max(...lookup.generate(4)), Math.min(...lookup.generate(4))); +console.log(Math.max(...lookup.generate(0)), Math.min(...lookup.generate(1))); diff --git a/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json new file mode 100644 index 000000000..2aa0f8038 --- /dev/null +++ b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json @@ -0,0 +1 @@ +{ "plain": 3384, "san": 4732 } From 6134dc4c8915c27b28b40ede5d5d673309a6d41f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:20:40 -0500 Subject: [PATCH 22/30] Run two-counter while loops as two-counter for loops --- packages/compiler/src/backend/llvm/emitter.ts | 5 +- .../compiler/src/ir/integer-loops.test.ts | 10 + packages/compiler/src/ir/integer-loops.ts | 19 +- .../src/ir/paired-while-loops.test.ts | 182 +++++++++++ .../compiler/src/ir/paired-while-loops.ts | 307 ++++++++++++++++++ .../corpus/7144-paired-while-counters.ts.json | 6 + tests/corpus/7144-paired-while-counters.ts | 244 ++++++++++++++ .../7144-paired-while-counters.ts.json | 1 + 8 files changed, 765 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/src/ir/paired-while-loops.test.ts create mode 100644 packages/compiler/src/ir/paired-while-loops.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json create mode 100644 tests/corpus/7144-paired-while-counters.ts create mode 100644 tests/harness/corpus-costs/7144-paired-while-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index efe6097df..dd114b525 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -92,6 +92,7 @@ import { type IntegerRanges, } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; +import { runPairedWhileLoopsAsFor } from "../../ir/paired-while-loops.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; import { resolveSelfCaptures } from "../../ir/self-captures.js"; import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js"; @@ -527,7 +528,9 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { resolveSelfCaptures( hoistFinalFieldReads( freezeInitOnlyGlobals( - holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)), + holdCharLocalsAsUnits( + dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)), + ), ), ), ), diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index b6ac27e3a..2cca43a00 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -402,6 +402,16 @@ test("paired counters closing in on each other run as integers", () => { guarded: false, range: { min: 0, max: 9 }, }); + // `hi > lo` and `hi >= lo` are `lo < hi` and `lo <= hi`. + for (const op of [">", ">="] as const) { + const reversed = paired(number(0), number(9)); + reversed.cond = { kind: "bin", op, left: ref("hi"), right: ref("lo"), type: BOOL, loc }; + expect(matchIntegerPairedForLoop(reversed, vars)).toMatchObject({ + low: { localId: "lo", step: 1 }, + high: { localId: "hi", step: -1 }, + inclusive: op === ">=", + }); + } // A start without a known range is tested at run time. expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ high: { startGuarded: true }, diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index f54f6618b..d6942718a 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -382,9 +382,9 @@ export function matchIntegerCountedForLoop( * * The first counter only grows and the second only shrinks, by positive * literal steps, the body writes neither, and the loop runs while the first - * is below (or at most) the second. Every body entry therefore holds both - * between the first start and the second start: exact integers when the - * starts are. A start without a proven integer range is tested at run time + * is below (or at most) the second, written either way round (`hi > lo`). + * Every body entry therefore holds both between the first start and the + * second start: exact integers when the starts are. A start without a proven integer range is tested at run time * on a versioned loop, as a counted loop's is. */ export interface IntegerPairedCounter { localId: string; @@ -412,10 +412,13 @@ export function matchIntegerPairedForLoop( cond = stmt.cond; if (init?.kind !== "block" || init.body.length !== 2) return null; if (update?.kind !== "block" || update.body.length !== 2) return null; - if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; - if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; - const lowId = cond.left.localId, - highId = cond.right.localId; + if (cond?.kind !== "bin" || cond.left.kind !== "varRef" || cond.right.kind !== "varRef") + return null; + const below = cond.op === "<" || cond.op === "<="; + if (!below && cond.op !== ">" && cond.op !== ">=") return null; + // `hi > lo` is `lo < hi`. + const lowId = below ? cond.left.localId : cond.right.localId, + highId = below ? cond.right.localId : cond.left.localId; if (lowId === highId) return null; const counter = (id: string): IntegerPairedCounter | null => { const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); @@ -485,7 +488,7 @@ export function matchIntegerPairedForLoop( return { low, high, - inclusive: cond.op === "<=", + inclusive: cond.op === "<=" || cond.op === ">=", guarded, // A loop whose starts already fail the condition never enters the body. range: { diff --git a/packages/compiler/src/ir/paired-while-loops.test.ts b/packages/compiler/src/ir/paired-while-loops.test.ts new file mode 100644 index 000000000..e0a69afa3 --- /dev/null +++ b/packages/compiler/src/ir/paired-while-loops.test.ts @@ -0,0 +1,182 @@ +import { expect, test } from "vitest"; +import { + BOOL, + F64, + arrayOf, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { runPairedWhileLoopsAsFor } from "./paired-while-loops.js"; + +const loc = { file: "paired-while-loops.ts", start: 0, end: 0 }; +const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); +const array = { kind: "varRef", localId: "a", type: arrayOf(F64), loc } as const; +const element = (index: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "arrayGet", arr: array, index, type: F64, loc }, + loc, +}); +const step = (id: string, op: "+" | "-"): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(1), type: F64, loc }, + loc, +}); +const declare = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc }); +const number_ = (id: string): IrLocal => ({ + id, + name: id.split(".")[0]!, + type: F64, + mutable: true, +}); + +function whileLoop( + body: IrStmt[] = [element(ref("lo.0")), element(ref("hi.0"))], + updates: IrStmt[] = [step("lo.0", "+"), step("hi.0", "-")], +): IrStmt & { kind: "while" } { + return { + kind: "while", + cond: { kind: "bin", op: "<", left: ref("lo.0"), right: ref("hi.0"), type: BOOL, loc }, + body: [...body, ...updates], + loc, + }; +} + +function program(body: IrStmt[], extra: Partial = {}): IrModule { + const fn: IrFunction = { + name: "f", + params: [{ localId: "n.0", name: "n", type: F64 }], + returnType: F64, + locals: [ + number_("n.0"), + number_("lo.0"), + number_("hi.0"), + { id: "a", name: "a", type: arrayOf(F64), mutable: false }, + ], + body, + loc, + ...extra, + }; + return { functions: [fn], globals: [] } as unknown as IrModule; +} + +const rewritten = (mod: IrModule): IrStmt[] => runPairedWhileLoopsAsFor(mod).functions[0]!.body; + +test("a two-counter while loop runs as a for loop over new counters", () => { + const mod = program([declare("lo.0", number(0)), declare("hi.0", ref("n.0")), whileLoop()]); + const out = runPairedWhileLoopsAsFor(mod).functions[0]!; + // The declarations stay; the loop's counters are new locals that start + // from them, and the trailing updates are the for loop's update. + expect(out.body.slice(0, 2)).toEqual(mod.functions[0]!.body.slice(0, 2)); + expect(out.body[2]).toMatchObject({ + kind: "for", + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo.1", init: { kind: "varRef", localId: "lo.0" } }, + { kind: "varDecl", localId: "hi.1", init: { kind: "varRef", localId: "hi.0" } }, + ], + }, + cond: { op: "<", left: { localId: "lo.1" }, right: { localId: "hi.1" } }, + update: { kind: "block", body: [{ localId: "lo.1" }, { localId: "hi.1" }] }, + body: [ + { expr: { kind: "arrayGet", index: { localId: "lo.1" } } }, + { expr: { kind: "arrayGet", index: { localId: "hi.1" } } }, + ], + }); + expect(out.locals.map((l) => l.id)).toEqual(["n.0", "lo.0", "hi.0", "a", "lo.1", "hi.1"]); +}); + +test("parameters, reversed comparisons, labels and either update order qualify", () => { + const loop = whileLoop(undefined, [step("hi.0", "-"), step("lo.0", "+")]); + loop.cond = { kind: "bin", op: ">=", left: ref("hi.0"), right: ref("lo.0"), type: BOOL, loc }; + loop.labels = ["outer"]; + const mod = program([declare("lo.0", number(0)), loop], { + params: [{ localId: "hi.0", name: "hi", type: F64 }], + }); + expect(rewritten(mod)[1]).toMatchObject({ + kind: "for", + labels: ["outer"], + cond: { op: ">=", left: { localId: "hi.1" }, right: { localId: "lo.1" } }, + update: { body: [{ localId: "hi.1" }, { localId: "lo.1" }] }, + }); +}); + +test("loops the for form would change, or whose counters are used elsewhere, stay", () => { + const decls = [declare("lo.0", number(0)), declare("hi.0", ref("n.0"))]; + const kept = (body: IrStmt[], extra: Partial = {}): void => { + const mod = program(body, extra); + expect(runPairedWhileLoopsAsFor(mod)).toBe(mod); + }; + // A continue that binds to the loop skips the trailing updates. + kept([...decls, whileLoop([{ kind: "continue", loc }, element(ref("lo.0"))])]); + const labeled = whileLoop([{ kind: "continue", label: "outer", loc }, element(ref("lo.0"))]); + labeled.labels = ["outer"]; + kept([...decls, labeled]); + // A counter read after the loop, written before it, or declared twice. + kept([...decls, whileLoop(), { kind: "return", value: ref("lo.0"), loc }]); + kept([...decls, step("hi.0", "-"), whileLoop()]); + kept([...decls, declare("lo.0", number(1)), whileLoop()]); + // A declaration outside an enclosing loop: a later entry would resume + // from the values the previous run left. + kept([ + ...decls, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [whileLoop()], + loc, + }, + ]); + // A parameter counter inside another loop, for the same reason. + const outerLoop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [whileLoop()], + loc, + }; + kept([outerLoop], { + params: [ + { localId: "lo.0", name: "lo", type: F64 }, + { localId: "hi.0", name: "hi", type: F64 }, + ], + }); + // A counter that was never declared, or that this function captures. + kept([declare("lo.0", number(0)), whileLoop()]); + kept([...decls, whileLoop()], { captures: [{ localId: "hi.0", name: "hi", type: F64 }] }); + // The body writes a counter, a counter is boxed, or no counter is an index. + kept([...decls, whileLoop([element(ref("lo.0")), step("hi.0", "-")])]); + const boxed = program([...decls, whileLoop()]); + boxed.functions[0]!.locals[2] = { ...number_("hi.0"), boxed: true }; + expect(runPairedWhileLoopsAsFor(boxed)).toBe(boxed); + kept([...decls, whileLoop([])]); +}); + +test("a continue in a nested loop keeps the outer loop eligible", () => { + const inner: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: false, type: BOOL, loc }, + body: [{ kind: "continue", loc }], + loc, + }; + // Literal starts have known ranges, so the loop needs no start test and + // may hold a nested loop. + const mod = program([ + declare("lo.0", number(0)), + declare("hi.0", number(9)), + whileLoop([inner, element(ref("lo.0"))]), + ]); + expect(rewritten(mod)[2]).toMatchObject({ kind: "for", body: [{ kind: "while" }, {}] }); + // A start without a known range is tested at run time, on a loop that + // may not nest another. + const guarded = program([ + declare("lo.0", number(0)), + declare("hi.0", ref("n.0")), + whileLoop([inner, element(ref("lo.0"))]), + ]); + expect(runPairedWhileLoopsAsFor(guarded)).toBe(guarded); +}); diff --git a/packages/compiler/src/ir/paired-while-loops.ts b/packages/compiler/src/ir/paired-while-loops.ts new file mode 100644 index 000000000..e0a994418 --- /dev/null +++ b/packages/compiler/src/ir/paired-while-loops.ts @@ -0,0 +1,307 @@ +import { matchIntegerPairedForLoop } from "./integer-loops.js"; +import { analyzeIntegerRanges, type IntegerRange } from "./integer-ranges.js"; +import { + F64, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; + +type WhileLoop = IrStmt & { kind: "while" }; + +/** Two-counter while loops run as two-counter for loops. + * + * let lo = a, hi = b; + * while (lo < hi) { ...; lo++; hi--; } + * + * runs as `for (let l = lo, h = hi; l < h; l++, h--) { ... }`, whose + * counters the backend stores as integers (matchIntegerPairedForLoop). The + * loop qualifies when: + * - its condition compares the two counters (`<`, `<=`, `>` or `>=`); + * - its body ends with one update of each counter; + * - no `continue` in the body binds to it (a for loop's `continue` runs the + * update, a while loop's skips the body's last statements); + * - the function mentions neither counter outside the loop, except in one + * declaration or as a parameter; + * - every entry starts from the declared or passed values: a declaration is + * an earlier statement of the loop's own statement list, which runs before + * each entry, and a loop over a parameter is in no other loop. + * + * The for loop's counters are new locals that start from the counters' + * values on entry, so the declarations stay where they are and nothing + * between them and the loop moves. The original counters are not read + * after the loop, so they need no final value. A loop that the paired match + * would refuse is left as it is. */ +export function runPairedWhileLoopsAsFor(mod: IrModule): IrModule { + let changed = false; + const functions = mod.functions.map((fn) => { + const next = rewriteFunction(fn); + if (next !== fn) changed = true; + return next; + }); + return changed ? { ...mod, functions } : mod; +} + +/** The local a statement steps: `x++`, `x--` or `x = ...`. */ +function updatedLocal(s: IrStmt): string | null { + if (s.kind === "assign") return s.localId; + if (s.kind === "exprStmt" && s.expr.kind === "incDec") return s.expr.localId; + return null; +} + +/** The counters of a loop with the qualifying condition and trailing + * updates, or null. */ +function loopCounters(loop: WhileLoop, locals: ReadonlyMap): string[] | null { + const cond = loop.cond; + if (cond.kind !== "bin") return null; + if (cond.op !== "<" && cond.op !== "<=" && cond.op !== ">" && cond.op !== ">=") return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const counters = [cond.left.localId, cond.right.localId]; + if (counters[0] === counters[1]) return null; + const n = loop.body.length; + if (n < 2) return null; + const updated = [updatedLocal(loop.body[n - 2]!), updatedLocal(loop.body[n - 1]!)]; + if (!counters.every((id) => updated.includes(id))) return null; + for (const id of counters) { + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + } + return counters; +} + +function isLoop(s: IrStmt): boolean { + return s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf"; +} + +/** True when a `continue` in the body binds to the loop: an unlabeled one + * outside nested loops, or one that names a label of the loop. */ +function continuesLoop(loop: WhileLoop): boolean { + const labels = loop.labels ?? []; + let depth = 0; + let found = false; + const expr = (e: IrExpr): boolean => !found && everyExprChild(e, expr, stmt); + const stmt = (s: IrStmt): boolean => { + if (found) return false; + if (s.kind === "continue") { + if (s.label === undefined ? depth === 0 : labels.includes(s.label)) found = true; + return !found; + } + const nested = isLoop(s); + if (nested) depth++; + const ok = everyStmtChild(s, expr, stmt); + if (nested) depth--; + return ok; + }; + loop.body.every(stmt); + return found; +} + +/** Mentions of each id in `node`, in any field (a string literal equal to + * an id counts too, which only refuses more loops). */ +function countMentions( + node: unknown, + ids: ReadonlySet, + mentions: Map, +): void { + if (typeof node === "string") { + if (ids.has(node)) mentions.set(node, (mentions.get(node) ?? 0) + 1); + } else if (Array.isArray(node)) { + for (const item of node) countMentions(item, ids, mentions); + } else if (node !== null && typeof node === "object") { + const record = node as Record; + for (const key in record) + if (key !== "loc" && key !== "type") countMentions(record[key], ids, mentions); + } +} + +/** A loop with the qualifying shape, the statements before it in its + * statement list, and whether it is inside another loop. */ +interface Candidate { + loop: WhileLoop; + counters: string[]; + before: readonly IrStmt[]; + nested: boolean; +} + +function findCandidates(fn: IrFunction, locals: ReadonlyMap): Candidate[] { + const candidates: Candidate[] = []; + let depth = 0; + const list = (body: readonly IrStmt[]): void => { + body.forEach((s, i) => { + if (s.kind === "while") { + const counters = loopCounters(s, locals); + if (counters !== null && !continuesLoop(s)) + candidates.push({ loop: s, counters, before: body.slice(0, i), nested: depth > 0 }); + } + visit(s); + }); + }; + // Statement lists inside expressions are not searched: their loops stay. + const visit = (s: IrStmt): void => { + const loop = isLoop(s); + if (loop) depth++; + switch (s.kind) { + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + list(s.body); + break; + case "if": + list(s.then); + if (s.else_ !== null) list(s.else_); + break; + case "switch": + for (const c of s.cases) list(c.body); + break; + case "tryCatch": + list(s.tryBody); + if (s.catchBody !== null) list(s.catchBody); + if (s.finallyBody !== null) list(s.finallyBody); + break; + default: + break; + } + if (loop) depth--; + }; + list(fn.body); + return candidates; +} + +function rewriteFunction(fn: IrFunction): IrFunction { + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const candidates = findCandidates(fn, locals); + if (candidates.length === 0) return fn; + + const ids = new Set(); + for (const c of candidates) for (const id of c.counters) ids.add(id); + const total = new Map(); + countMentions(fn.body, ids, total); + const params = new Set(fn.params.map((p) => p.localId)); + // A counter this function reaches through its environment. + const outer = new Map(); + countMentions([fn.captures ?? [], fn.classCaptures ?? []], ids, outer); + + let ranges: ReadonlyMap | null = null; + const used = new Set(fn.locals.map((l) => l.id)); + const added: IrLocal[] = []; + const rewritten = new Map>(); + for (const { loop, counters, before, nested } of candidates) { + const inside = new Map(); + countMentions(loop, new Set(counters), inside); + // Each counter's start: its declaration's initializer, or null for a + // parameter. + const starts = new Map(); + for (const id of counters) { + if ((outer.get(id) ?? 0) !== 0) break; + const elsewhere = (total.get(id) ?? 0) - (inside.get(id) ?? 0); + if (params.has(id)) { + if (elsewhere === 0 && !nested) starts.set(id, null); + continue; + } + const decl = before.find((s) => s.kind === "varDecl" && s.localId === id); + if (elsewhere === 1 && decl?.kind === "varDecl" && decl.init !== null) + starts.set(id, decl.init); + } + if (starts.size !== counters.length) continue; + const renamed = new Map(); + const fresh: IrLocal[] = []; + for (const id of counters) { + const local = locals.get(id)!; + let k = 0; + while (used.has(`${local.name}.${k}`)) k++; + const next = `${local.name}.${k}`; + used.add(next); + renamed.set(id, next); + fresh.push({ ...local, id: next }); + } + const candidate = pairedForLoop(loop, renamed); + const forLocals = new Map(locals); + for (const local of fresh) forLocals.set(local.id, local); + // A start reads its counter on entry, which holds the initializer's + // value, with that value's range. + ranges ??= analyzeIntegerRanges(fn); + const startRanges = new Map(); + if (candidate.init?.kind === "block") + for (const decl of candidate.init.body) { + if (decl.kind !== "varDecl" || decl.init?.kind !== "varRef") continue; + const init = starts.get(decl.init.localId); + if (!init) continue; + const literal = + init.kind === "numLit" && Number.isSafeInteger(init.value) && !Object.is(init.value, -0); + startRanges.set( + decl.init, + literal ? { min: init.value, max: init.value } : (ranges.get(init) ?? null), + ); + } + if (matchIntegerPairedForLoop(candidate, forLocals, startRanges) === null) { + for (const local of fresh) used.delete(local.id); + continue; + } + rewritten.set(loop, renamed); + added.push(...fresh); + } + if (rewritten.size === 0) return fn; + + const expr = (e: IrExpr): IrExpr => mapExprChildren(e, expr, stmt); + const stmt = (s: IrStmt): IrStmt => { + const next = mapStmtChildren(s, expr, stmt); + const renamed = s.kind === "while" ? rewritten.get(s) : undefined; + // The body may hold rewritten loops of its own. + return renamed === undefined ? next : pairedForLoop(next as WhileLoop, renamed); + }; + return { ...fn, body: fn.body.map(stmt), locals: [...fn.locals, ...added] }; +} + +/** The for loop over the renamed counters: their starts read the original + * counters, and the body's two trailing updates become the update. */ +function pairedForLoop( + loop: WhileLoop, + renamed: ReadonlyMap, +): IrStmt & { kind: "for" } { + const expr = (e: IrExpr): IrExpr => { + const next = mapExprChildren(e, expr, stmt); + if (next.kind === "varRef" || next.kind === "incDec" || next.kind === "assignExpr") { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } + return next; + }; + const stmt = (s: IrStmt): IrStmt => { + const next = mapStmtChildren(s, expr, stmt); + if ( + next.kind === "varDecl" || + next.kind === "assign" || + next.kind === "forOf" || + next.kind === "rethrow" + ) { + const id = renamed.get(next.localId); + if (id !== undefined) return { ...next, localId: id }; + } + return next; + }; + const loc = loop.loc; + const n = loop.body.length; + const declarations: IrStmt[] = []; + for (const [id, next] of renamed) + declarations.push({ + kind: "varDecl", + localId: next, + init: { kind: "varRef", localId: id, type: F64, loc }, + loc, + }); + const result: IrStmt & { kind: "for" } = { + kind: "for", + init: { kind: "block", body: declarations, loc }, + cond: expr(loop.cond), + update: { kind: "block", body: loop.body.slice(n - 2).map(stmt), loc }, + body: loop.body.slice(0, n - 2).map(stmt), + loc, + }; + if (loop.labels !== undefined) result.labels = loop.labels; + return result; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json new file mode 100644 index 000000000..678cbd8b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7144-paired-while-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7144-paired-while-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7144-paired-while-counters.ts b/tests/corpus/7144-paired-while-counters.ts new file mode 100644 index 000000000..2198d213f --- /dev/null +++ b/tests/corpus/7144-paired-while-counters.ts @@ -0,0 +1,244 @@ +// While loops with two counters closing in on each other (`lo++; hi--;` at +// the end of the body): reversals over arrays, typed arrays and strings, +// either comparison direction, inclusive bounds, larger steps, parameters as +// counters, early exits, unproven starts, and the shapes that keep the loop +// as it is (a continue, a counter read after the loop, a captured counter, +// a counter the body writes). +function reverse(a: number[], from: number, to: number): number[] { + let lo = from; + let hi = to; + while (lo < hi) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + lo++; + hi--; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +console.log(reverse(base(), -0, 2).join(","), reverse(base(), 7, -0).join(",")); + +// The parameters are the counters; the updates come in the other order. +function reverseRange(a: Float64Array, lo: number, hi: number): string { + while (hi > lo) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + hi -= 1; + ++lo; + } + return a.join(","); +} +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 0, 5)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), 1, 3.5)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), -2, 2)); +console.log(reverseRange(new Float64Array([1, 2, 3, 4, 5, 6]), NaN, 4), reverseRange(new Float64Array([1, 2]), 0, NaN)); + +function bytesReversed(text: string): string { + const bytes = new TextEncoder().encode(text); + let i = 0, + j = bytes.length - 1; + while (i < j) { + const t = bytes[i]!; + bytes[i] = bytes[j]!; + bytes[j] = t; + i++; + j--; + } + return new TextDecoder().decode(bytes); +} +console.log(bytesReversed("hello, world"), bytesReversed(""), bytesReversed("x")); + +// Unproven starts read through charCodeAt (which truncates its index). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + let lo = from; + let hi = to; + while (lo <= hi) { + out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + lo += 2; + hi -= 3; + } + return out.join(","); +} +const word = "abcdefghijkl"; +console.log(codes(word, 0, 11), "|", codes(word, 0.5, 9.25), "|", codes(word, 4, 4)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53), "|", codes(word, -3, 2), "|", codes(word, 5, 1)); + +// Negative zero starts stay negative zero. +function signs(text: string, from: number): string { + const out: string[] = []; + let lo = from; + let hi = text.length - 1; + while (lo < hi) { + out.push(`${Object.is(lo, -0)}:${text.charCodeAt(lo)}`); + lo++; + hi--; + } + let a = -0; + let b = 2; + while (a < b) { + out.push(`${Object.is(a, -0)}:${text.charCodeAt(b)}`); + a++; + b--; + } + return out.join(" "); +} +console.log(signs("abcd", -0), signs("abcd", 0)); + +function isPalindrome(s: string): boolean { + let i = 0; + let j = s.length - 1; + while (i < j) { + if (s.charCodeAt(i) !== s.charCodeAt(j)) return false; + i++; + j--; + } + return true; +} +console.log(isPalindrome("racecar"), isPalindrome("abca"), isPalindrome(""), isPalindrome("ab")); + +function firstMismatch(a: number[]): number { + let found = -1; + let lo = 0; + let hi = a.length - 1; + while (lo < hi) { + const left = a[lo]!; + if (left !== a[hi]!) { + found = lo; + break; + } + lo++; + hi--; + } + return found; +} +console.log(firstMismatch([1, 2, 3, 2, 1]), firstMismatch([1, 2, 3, 4, 2, 1]), firstMismatch([])); + +// A labeled jump to an outer loop leaves the inner loop from its middle. +function rows(grid: number[][]): string { + const out: string[] = []; + outer: for (const row of grid) { + let lo = 0; + let hi = row.length - 1; + while (lo < hi) { + const left = row[lo]!; + const right = row[hi]!; + if (left === 0) continue outer; + if (right === 0) break outer; + out.push(`${left}${right}`); + lo++; + hi--; + } + out.push("|"); + } + return out.join(" "); +} +console.log(rows([[1, 2, 3, 4], [0, 5, 6], [7, 8, 9, 1], [2, 3, 0, 4]])); + +// Repeated entries: the counters are declared inside an outer loop. Each +// pass reverses the prefix that ends at the index the first element holds, +// until the first element is 0, and the passes are counted. +function prefixReversals(values: number[]): number { + const copy = values.slice(); + let count = 0; + let end = copy[0]!; + while (end !== 0) { + let i = 0; + let j = end; + while (i < j) { + const t = copy[i]!; + copy[i] = copy[j]!; + copy[j] = t; + i++; + j--; + } + count++; + end = copy[0]!; + } + return count; +} +console.log( + prefixReversals([3, 1, 2, 0]), + prefixReversals([0, 1, 2]), + prefixReversals([4, 3, 2, 1, 0]), + prefixReversals([2, 0, 1]), +); + +// Holes and present undefined values move as they are. +const sparse: (number | undefined)[] = [1, , 3, undefined, 5]; +{ + let lo = 0; + let hi = sparse.length - 1; + while (lo < hi) { + const t = sparse[lo]; + sparse[lo] = sparse[hi]; + sparse[hi] = t; + lo++; + hi--; + } +} +console.log(sparse, 1 in sparse, 3 in sparse); + +// These keep the while loop: a continue skips the trailing updates, a +// counter is read after the loop, a closure captures a counter, and the +// body writes a counter. +function skipping(n: number): string { + const out: number[] = []; + let lo = 0; + let hi = n; + while (lo < hi) { + if (lo % 3 === 0) { + lo += 2; + continue; + } + out.push(lo, hi); + lo++; + hi--; + } + return out.join(","); +} +console.log(skipping(12)); + +function meet(n: number): string { + let lo = 0; + let hi = n; + while (lo < hi) { + lo++; + hi--; + } + return `${lo}:${hi}`; +} +console.log(meet(9), meet(10), meet(0.5)); + +function captured(n: number): string { + const seen: (() => number)[] = []; + let lo = 0; + let hi = n; + while (lo < hi) { + seen.push(() => lo * 100 + hi); + lo++; + hi--; + } + return seen.map((f) => f()).join(","); +} +console.log(captured(5)); + +function jumps(a: number[]): string { + const out: number[] = []; + let lo = 0; + let hi = a.length - 1; + while (lo < hi) { + out.push(a[lo]!, a[hi]!); + if (a[lo]! > 5) hi = lo; + lo++; + hi--; + } + return out.join(","); +} +console.log(jumps([1, 2, 9, 4, 5, 6, 7, 8])); diff --git a/tests/harness/corpus-costs/7144-paired-while-counters.ts.json b/tests/harness/corpus-costs/7144-paired-while-counters.ts.json new file mode 100644 index 000000000..935249de9 --- /dev/null +++ b/tests/harness/corpus-costs/7144-paired-while-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4600 } From d027786c8050e35d239f0a5188fdbe5df46dc17a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:29:34 -0500 Subject: [PATCH 23/30] Convert Buffers and DataViews to BigInt through their own text BigInt of byte storage joined the elements with "," for every kind, so a Buffer, whose text is its UTF-8 decoding, and a DataView, whose text is "[object DataView]", converted as typed arrays. BigInt now parses the value's ToString, which the runtime value decides, as String does. This also covers a Buffer held in a Uint8Array slot. Corpus 7403 adds Buffers, a DataView, and a Buffer in a typed array slot. Refs #1647 --- .../src/frontend/lowering/lower-calls.ts | 14 +++++++------- tests/corpus/7403-bigint-of-typed-arrays.mjs | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 8424f28c4..e44f0a2bc 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2400,15 +2400,15 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) return { kind: "libCall", fn: "bigint.parse", args: [arg], type: BIGINT_T, loc }; if (arg.type.kind === "f64") return { kind: "libCall", fn: "bigint.fromF64", args: [arg], type: BIGINT_T, loc }; - // A typed array converts through ToPrimitive to its elements joined with - // ",", which BigInt then parses: one element reads as that element, none - // as 0n, and several throw the SyntaxError for the joined text. + // Byte storage converts through ToPrimitive, which BigInt then parses: a + // typed array's text is its elements joined with "," (one element reads + // as that element, none as 0n, several throw the SyntaxError for the + // joined text), a Buffer's is its UTF-8 decoding and a DataView's is + // "[object DataView]". The runtime value tells them apart, as for String. if (arg.type.kind === "bytes") { const text: IrExpr = { - kind: "bytesIntrinsic", - method: "join", - receiver: arg, - args: [{ kind: "strLit", value: ",", type: STRING, loc }], + kind: "toString", + operand: { kind: "dynFrom", value: arg, type: DYN, loc }, type: STRING, loc, }; diff --git a/tests/corpus/7403-bigint-of-typed-arrays.mjs b/tests/corpus/7403-bigint-of-typed-arrays.mjs index 77ef0cd2e..9cf813d9a 100644 --- a/tests/corpus/7403-bigint-of-typed-arrays.mjs +++ b/tests/corpus/7403-bigint-of-typed-arrays.mjs @@ -2,7 +2,9 @@ // "," and parses that text, as in Node: one element reads as its value, // an empty array as 0n, and anything else throws the SyntaxError for the // joined text. Every element kind, negative and clamped values, float -// formatting, views and the results in arithmetic. +// formatting, views and the results in arithmetic. A Buffer converts +// through its UTF-8 text instead, also when a typed array slot holds it, +// and a DataView through "[object DataView]". const one = [ new Int8Array([-5]), @@ -44,3 +46,17 @@ for (let i = 0; i < 100; i++) { total += BigInt(window) * 3n; } console.log("window", BigInt(window), total, BigInt(bytes.subarray(2)) ** 2n); + +function convert(label, make) { + try { + console.log(label, BigInt(make())); + } catch (e) { + console.log(label, e instanceof SyntaxError, e.message); + } +} +convert("buffer", () => Buffer.from("12")); +convert("spaced buffer", () => Buffer.from(" -7 ")); +convert("empty buffer", () => Buffer.alloc(0)); +convert("fraction buffer", () => Buffer.from("1.5")); +convert("data view", () => new DataView(new ArrayBuffer(2))); +for (const view of [new Uint8Array([5]), Buffer.from("64")]) console.log("slot", BigInt(view)); From 37ea2c9ce7d5c8f11ffab42c3ac79daa71b8e321 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:31:26 -0700 Subject: [PATCH 24/30] Keep arrays stored through a typed global view native A fresh array literal written to a checked-dynamic slot is built as a dynamic array. On a member of a typed view of the global object (`(globalThis as typeof globalThis & { cache?: number[] }).cache ??= []`) the program reads it back through that declaration, and each typed read extracted a new native copy. `cache().push(x)` was lost, and two reads were never the same array. Node keeps one array. When the literal is the right side of a write (`=`, `??=`, `||=`) to a member of the global object, and the member is declared as a native array, build the native array and store it boxed. Every typed read then extracts that same array, and untyped reads see it through its live capsule. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 46 +++++++++++++++++++ .../7366-typed-global-view-arrays.ts.json | 6 +++ tests/corpus/7366-typed-global-view-arrays.ts | 32 +++++++++++++ .../7366-typed-global-view-arrays.ts.json | 1 + 4 files changed, 85 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json create mode 100644 tests/corpus/7366-typed-global-view-arrays.ts create mode 100644 tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4c417e86e..554b703e9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10609,6 +10609,38 @@ export class Lowerer { /** Lower for a known destination, then apply ordinary value coercion. * Fresh arrays and records use the destination layout directly. */ + /** `value` is the right side of a write to a member of a typed view of the + * global object (`(globalThis as typeof globalThis & { k?: T }).k = value`, + * also through `??=` and `||=`). */ + private globalViewSlot(value: ts.Expression): boolean { + const write = value.parent; + if ( + !ts.isBinaryExpression(write) || + write.right !== value || + (write.operatorToken.kind !== ts.SyntaxKind.EqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) + ) + return false; + let target = write.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) + return false; + const receiver = this.typeOf(target.expression); + const isGlobal = (type: ts.Type): boolean => { + const symbol = type.getSymbol(); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "globalThis" && + symbol.declarations.length === 0 + ); + }; + return ( + isGlobal(receiver) || + (receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal)) + ); + } + lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr { // These wrappers do not change the value or its inferred type. Keep a // fresh literal visible to destination-directed construction. Assertions @@ -10674,6 +10706,20 @@ export class Lowerer { (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { + // A checked slot that the program declares as a native array (a + // member of a typed view of the global object, `typeof globalThis & + // { cache?: number[] }`) is read back through that declaration. Build + // the native array and store it boxed: every typed read then extracts + // that same array (a dynamic literal would be copied on each read, + // losing writes such as push). + const slotType = this.globalViewSlot(x) ? this.checker.getContextualType(x) : undefined; + const declared = slotType && this.checker.getNonNullableType(slotType); + const native = + declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) + ? this.mapTypeOf(declared) + : null; + if (native?.kind === "array") + return this.coerceInto(node, this.lowerArrayLiteral(x, native), DYN); return { kind: "dynArrLit", elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..49da80b55 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7366-typed-global-view-arrays.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7366-typed-global-view-arrays.ts b/tests/corpus/7366-typed-global-view-arrays.ts new file mode 100644 index 000000000..3b24620d5 --- /dev/null +++ b/tests/corpus/7366-typed-global-view-arrays.ts @@ -0,0 +1,32 @@ +// Arrays stored on the global object through a typed view are the same +// arrays every typed read returns: writes through one read (push, index +// assignment, sort) are visible through the next, as in Node. +type Store = typeof globalThis & { __exampleCache?: number[]; __exampleNames?: string[] }; + +function cache(): number[] { + const g = globalThis as Store; + g.__exampleCache ??= [1]; + return g.__exampleCache; +} + +cache().push(2); +cache().push(3); +cache()[0] = 10; +console.log(cache().length, cache().join(","), cache() === cache()); + +const view = globalThis as Store; +view.__exampleNames = ["b", "a"]; +const names = view.__exampleNames; +names.push("c"); +names.sort(); +console.log(view.__exampleNames.join(","), names === view.__exampleNames); + +// the same array seen through an untyped read +const raw = (globalThis as unknown as Record)["__exampleCache"]; +console.log(Array.isArray(raw), JSON.stringify(raw), raw === cache()); + +// a literal of another element type stays its own value +const counts = globalThis as typeof globalThis & { __exampleCounts?: number[][] }; +counts.__exampleCounts = [[1], [2, 3]]; +counts.__exampleCounts[1]!.push(4); +console.log(JSON.stringify(counts.__exampleCounts)); diff --git a/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 } From 390ac6f32691f75bc15ccfd4469ac1904e4ad044 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:27:16 -0700 Subject: [PATCH 25/30] Call observed class methods natively while their slot is unchanged Any computed write with a key TypeScript cannot know (zod's `curr[el] = ...` in ZodError.js, for one) puts every class method name in the method census. Every typed call of such a method then went through the observable prototype slot: a dynamic call with boxed arguments, followed by a checked conversion of the result (for a `Product[]` result, a deep walk of every element). Bundling untyped JavaScript made unrelated typed code several times slower per call. The prototype data now keeps each observed method's original value in a global. A typed, non-virtual call of a TypeScript method still reads the slot first, as before, and calls the method natively when the slot holds that original value. A replacement, on the prototype or as an own property, still takes the dynamic call. Virtual calls and methods declared in unchecked JavaScript keep the dynamic call. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-callbacks.ts | 39 ++++++++++ .../src/frontend/lowering/class-prototypes.ts | 13 +++- .../src/frontend/lowering/lower-calls.ts | 15 ++++ .../src/frontend/lowering/lower-classes.ts | 4 + .../test/observed-method-guard.test.ts | 77 +++++++++++++++++++ .../main.ts.json | 7 ++ .../main.ts | 59 ++++++++++++++ .../untyped.js | 15 ++++ .../main.ts.json | 1 + 9 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/observed-method-guard.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json create mode 100644 tests/corpus/7365-class-methods-untyped-computed-writes/main.ts create mode 100644 tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js create mode 100644 tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index b51158022..0bda0068e 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -164,6 +164,7 @@ export function classCallbackCall( fallback: IrExpr, loc: SrcLoc, calleeName = name, + original?: { originalId: () => string | undefined; directCall: () => IrExpr }, ): IrExpr { const bag = lowerer.declareHiddenLocal("%callbackBag", DYN); const present = lowerer.declareHiddenLocal("%callbackPresent", BOOL); @@ -235,6 +236,44 @@ export function classCallbackCall( answer = fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); } + // Read after observedSlot built the prototype data and its originals. + const originalId = observed ? original?.originalId() : undefined; + if (original && originalId !== undefined) { + // The slot still holds the method's original value: calling it is + // calling the method, so the call stays native (no boxed arguments, + // dynamic call or checked result). A replacement takes the dynamic call. + const unchanged: IrExpr = { + kind: "libCall", + fn: "dyn.sameValue", + args: [varRef(callback.id, DYN, loc), varRef(originalId, DYN, loc)], + type: BOOL, + loc, + }; + answer = + fallback.type.kind === "void" + ? { + kind: "ternary", + cond: unchanged, + then: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: original.directCall(), loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: call, + type: DYN, + loc, + } + : { + kind: "ternary", + cond: unchanged, + then: original.directCall(), + else_: answer, + type: answer.type, + loc, + }; + } return { kind: "seqExpr", stmts: [ diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 2a05cf977..3ecdd84aa 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -207,12 +207,21 @@ export function classPrototypeData( // promise-returning signature) gets no data slot. const boxedValue = lowerer.coerceToExpected(compiled, DYN); if (boxedValue.type.kind !== "dyn") continue; + const originalId = `%g.${info.def.prototypeDataHelper}.original.${method}`; + lowerer.globalsList.push({ + id: originalId, + name: `${info.def.jsName ?? info.def.name}.prototype.${method}.original`, + type: DYN, + mutable: true, + }); + info.prototypeOriginals ??= new Map(); + info.prototypeOriginals.set(method, originalId); const descriptor: IrExpr = { kind: "dynObjLit", fields: [ { key: { kind: "strLit", value: "value", type: STRING, loc }, - value: boxedValue, + value: varRef(originalId, DYN, loc), }, { key: { kind: "strLit", value: "writable", type: STRING, loc }, @@ -227,6 +236,8 @@ export function classPrototypeData( loc, }; const init = helper.body[0]!; + if (init.kind === "if") + init.then.push({ kind: "assign", localId: originalId, value: boxedValue, loc }); if (init.kind === "if") init.then.push({ kind: "exprStmt", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 14d94ad24..493d979f5 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10195,6 +10195,20 @@ export function lowerObjectMethodCall( type: found.sig.ret, loc, }; + // The declaring class's original value, built with the observed slot. + // An override below (a virtual call) keeps the dynamic call: a slot + // reassigned to an ancestor's method must not reach the override. So do + // methods declared in unchecked JavaScript, whose results are inferred. + const declarerFile = found.declarer.decl ? ts.getSourceFileOfNode(found.declarer.decl) : null; + const original = + virtual || + !declarerFile || + (isJsSourceFile(declarerFile) && !lowerer.checkedJsFile(declarerFile)) + ? undefined + : { + originalId: (): string | undefined => found.declarer.prototypeOriginals?.get(method), + directCall: (): IrExpr => fallback, + }; const result = classCallbackCall( lowerer, value, @@ -10203,6 +10217,7 @@ export function lowerObjectMethodCall( fallback, loc, ts.getTextOfNode(access), + original, ); return { kind: "seqExpr", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6dd0124bd..29afbb4fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -478,6 +478,10 @@ export interface ClassInfo { prototypeMethodValues?: Map; prototypeReflectionReady?: true; prototypeObservedMethodsReady?: true; + /** The globals holding each observed method's original prototype value + * (set when the prototype data is built), by method name. A typed call + * whose slot still holds that value calls the method natively. */ + prototypeOriginals?: Map; prototypeReflectionRefusal?: string; localClass?: { context: FnCtx | null; diff --git a/packages/compiler/test/observed-method-guard.test.ts b/packages/compiler/test/observed-method-guard.test.ts new file mode 100644 index 000000000..5964c482b --- /dev/null +++ b/packages/compiler/test/observed-method-guard.test.ts @@ -0,0 +1,77 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; + +const dirs: string[] = []; + +afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +type IrRecord = Record; + +function recordsOf(value: unknown, out: IrRecord[] = []): IrRecord[] { + if (value === null || typeof value !== "object") return out; + if (Array.isArray(value)) { + for (const item of value) recordsOf(item, out); + return out; + } + const record = value as IrRecord; + out.push(record); + for (const child of Object.values(record)) recordsOf(child, out); + return out; +} + +// An untyped computed write puts every method name in the census, so typed +// calls read the observable slot. While the slot holds the declared method, +// the call is the native method call, not a dynamic call of the slot. +test("typed calls of an observed method call it natively while its slot is unchanged", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-")); + dirs.push(dir); + const entry = join(dir, "main.ts"); + const outDir = join(dir, ".scriptc"); + const outPath = join(outDir, "main.ir.json"); + await writeFile( + join(dir, "untyped.js"), + "export function put(target, key, value) { target[key] = value; }\n", + ); + await writeFile( + entry, + [ + 'import { put } from "./untyped.js";', + "class Repo {", + " items: number[] = [];", + " total(): number { let s = 0; for (const n of this.items) s += n; return s; }", + "}", + "const repo = new Repo();", + "repo.items.push(1, 2);", + "put({}, String(Math.random()), 1);", + "export function sum(r: Repo): number { return r.total(); }", + "console.log(sum(repo));", + "", + ].join("\n"), + ); + const result = await compile(entry, { outDir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const module = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(module)).toEqual([]); + const sum = module.functions.find((fn) => fn.name.endsWith("sum")); + expect(sum).toBeDefined(); + const records = recordsOf(sum); + const guards = records.filter( + (record) => + record.kind === "ternary" && + (record.cond as IrRecord | undefined)?.kind === "libCall" && + (record.cond as IrRecord).fn === "dyn.sameValue", + ); + expect(guards).toHaveLength(1); + const then = guards[0]!.then as IrRecord; + expect(then.kind).toBe("call"); + expect(String(then.callee)).toMatch(/Repo\.total$/); + expect(module.globals?.some((global) => global.name === "Repo.prototype.total.original")).toBe( + true, + ); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json new file mode 100644 index 000000000..58cdacb62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js", + "/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts new file mode 100644 index 000000000..f7449f1af --- /dev/null +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts @@ -0,0 +1,59 @@ +// Typed classes next to untyped computed writes. Calls through a typed +// receiver keep the method while its prototype slot holds the declared +// method, and observe a replacement made by untyped code (on the prototype +// or as an own property of one instance) exactly as Node does. +import { patch, tally, unpatch } from "./untyped.js"; + +class Repository { + private items = new Map(); + add(name: string, cents: number): number { + this.items.set(name, cents); + return this.items.size; + } + list(): string[] { + return [...this.items.keys()]; + } + total(): number { + let sum = 0; + for (const cents of this.items.values()) sum += cents; + return sum; + } +} + +const words: string[] = ["list", "add", "list", "total"]; +console.log(JSON.stringify(tally(words))); + +const repo = new Repository(); +repo.add("keyboard", 12900); +repo.add("mouse", 4900); +let sum = 0; +for (let i = 0; i < 1000; i++) sum += repo.total(); +console.log(repo.list().join(","), sum); + +// untyped code replaces the method on the prototype +const proto: unknown = Object.getPrototypeOf(repo); +patch(proto, "total", function (this: Repository): number { + return -1; +}); +console.log(repo.total(), new Repository().total()); +unpatch(proto, "total"); +try { + repo.total(); +} catch (error) { + console.log("after delete:", error instanceof TypeError); +} + +// an own replacement on one instance shadows the prototype for that instance only +const other = new Repository(); +other.add("cable", 900); +patch(other, "list", () => ["shadowed"]); +console.log(other.list().join(","), repo.list().join(",")); +unpatch(other, "list"); +console.log(other.list().join(",")); + +// restoring the original value through the prototype brings back the method +const saved: unknown = (proto as Record)["list"]; +patch(proto, "list", () => ["patched"]); +console.log(repo.list().join(",")); +patch(proto, "list", saved); +console.log(repo.list().join(",")); diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js b/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js new file mode 100644 index 000000000..3c0d20189 --- /dev/null +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/untyped.js @@ -0,0 +1,15 @@ +// Untyped helpers with computed writes whose keys TypeScript cannot know. +// They put every class method name in play for the method census. +export function tally(words) { + const counts = {}; + for (const word of words) counts[word] = (counts[word] || 0) + 1; + return counts; +} + +export function patch(target, name, impl) { + target[name] = impl; +} + +export function unpatch(target, name) { + delete target[name]; +} diff --git a/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json b/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json new file mode 100644 index 000000000..0347bbd22 --- /dev/null +++ b/tests/harness/corpus-costs/7365-class-methods-untyped-computed-writes/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3400 } From 0314d9db3e0168cf3279995f979d67a610a32e08 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:52:25 -0500 Subject: [PATCH 26/30] Dispatch checked-dynamic method calls on method ids --- benchmarks/runtime/README.md | 1 + .../runtime/workloads/dynamic-methods.json | 6 + .../runtime/workloads/dynamic-methods.mjs | 36 +++ packages/cli/test/native-link-info.test.ts | 4 +- .../compiler/src/backend/llvm/emit-module.ts | 2 + packages/compiler/src/backend/llvm/emitter.ts | 24 ++ .../compiler/src/backend/llvm/expr-dynamic.ts | 8 +- packages/compiler/src/backend/llvm/js-ic.ts | 2 +- packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_bytes_dyn.c | 42 +-- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 22 +- packages/runtime/src/scr_dyn_invoke.c | 276 ++++++++++-------- packages/runtime/src/scr_dyn_methods.h | 160 ++++++++++ packages/runtime/src/scr_dyn_methods.test.ts | 10 + packages/runtime/src/scr_worker_events.c | 40 +-- scripts/gen-dyn-methods.mjs | 101 +++++++ scripts/zero-cost-compare.mjs | 4 +- .../self-hosting-native-toolchain.test.ts | 2 +- 25 files changed, 572 insertions(+), 188 deletions(-) create mode 100644 benchmarks/runtime/workloads/dynamic-methods.json create mode 100644 benchmarks/runtime/workloads/dynamic-methods.mjs create mode 100644 packages/runtime/src/scr_dyn_methods.h create mode 100644 packages/runtime/src/scr_dyn_methods.test.ts create mode 100644 scripts/gen-dyn-methods.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index e177e36c0..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -12,6 +12,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` | | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | | `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | +| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | diff --git a/benchmarks/runtime/workloads/dynamic-methods.json b/benchmarks/runtime/workloads/dynamic-methods.json new file mode 100644 index 000000000..fa27f4ea8 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/dynamic-methods.mjs", + "args": [ + "300000" + ] +} diff --git a/benchmarks/runtime/workloads/dynamic-methods.mjs b/benchmarks/runtime/workloads/dynamic-methods.mjs new file mode 100644 index 000000000..bbf217701 --- /dev/null +++ b/benchmarks/runtime/workloads/dynamic-methods.mjs @@ -0,0 +1,36 @@ +// Untyped JavaScript calling builtin methods on values it never declares +// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array +// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number +// toFixed, and calls of an object's own methods. Argument: the number of +// iterations. +const rounds = Number(process.argv[2] ?? "300000"); +const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"]; +const counts = new Map(); +const seen = new Set(); +const stack = []; +const recent = []; +const tally = { + total: 0, + add(value) { + this.total += value; + return this.total; + }, +}; +let checksum = 0; +for (let i = 0; i < rounds; i++) { + const word = words[i % words.length]; + const head = word.slice(0, 3).toUpperCase(); + const key = head.padStart(4, "-"); + if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1; + counts.set(key, (counts.get(key) ?? 0) + 1); + seen.add(i % 97); + if (seen.has(i % 89)) checksum += 2; + stack.push(i); + if (stack.length > 32) checksum += stack.pop() % 3; + recent.push(word); + if (recent.length > 4) recent.shift(); + if (recent.includes("gamma")) checksum += recent.indexOf("gamma"); + checksum += tally.add(i % 5) % 11; + if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0; +} +console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total); diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index bf5b3be14..dd3fe7af9 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v33"); + expect(error?.stderr).toContain("scr_runtime_abi_v35"); }); }); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index b6e489c19..16a1b1601 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -873,6 +873,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] { out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`); } if (emitter.mapOpsConsts.size > 0) out.push(``); + for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`); + if (emitter.methodIdSlots.size > 0) out.push(``); for (const line of ffiCallbacks.globals) out.push(line); if (ffiCallbacks.globals.length > 0) out.push(``); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index dd114b525..8a32c0f08 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1079,6 +1079,10 @@ export class LlEmitter { readonly cstrs = new Map(); /** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */ readonly mapOpsConsts = new Map(); + /** Method-id slots of checked-dynamic method calls + * (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the + * runtime fills with the name's id on the first call. */ + readonly methodIdSlots = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ @@ -2739,6 +2743,17 @@ export class LlEmitter { return `@${c.sym}`; } + /** The method-id slot for a constant method name (methodIdSlots). */ + methodIdSlot(name: string): string { + let sym = this.methodIdSlots.get(name); + if (!sym) { + sym = this.contentSymbol("sc_mid_", name); + this.methodIdSlots.set(name, sym); + } + this.journal?.entry("methodIdSlot", name, sym); + return `@${sym}`; + } + needBadTag(): void { this.needsBadTag = true; } @@ -4856,6 +4871,15 @@ export class LlEmitter { if (own !== value) names.set(value as string, own); return; } + case "methodIdSlot": { + let own = this.methodIdSlots.get(key); + if (own === undefined) { + own = this.contentSymbol("sc_mid_", key); + this.methodIdSlots.set(key, own); + } + if (own !== value) names.set(value as string, own); + return; + } case "classObj": addEntry(this.classObjs, key, value as { nameSym: string }); return; diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 9af677586..434435eae 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -250,8 +250,10 @@ export function emitDynamicExpr( } // Buffer.prototype method names take their scr_bytes_dyn.c entry, which // runs them on Buffer receivers and dispatches any other receiver. - const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared"; - host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); + // Each call passes its method name's id slot, so only the first call + // looks the name up. + const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -260,7 +262,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index 45c18b803..7516673cd 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue { /** dynInvoke's method read on an ordinary object through the site's * cache, else scr_dyn_prepare_method. Returns the owned method (or NULL, - * which scr_dyn_invoke_prepared answers with the full dispatch). */ + * which scr_dyn_invoke_prepared_id answers with the full dispatch). */ export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string { const B = host.B; const method = e.method; diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 7d3dcbc02..11647ab0c 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 33 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const; +export const RUNTIME_ABI_VERSION = 35 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v35" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 532051b88..cb3f8a5d9 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 18b04b9a6..17ac1db9e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0fd5249d3..ce1041c99 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v35"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v33"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v35"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e17312713..4802a7970 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 60ef9dc49..329aac00d 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index 280aa423c..c492d43b1 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -509,8 +509,9 @@ static bool scr_bytes_dyn_buffer(const ScrDyn *recv) { /* One entry per method family, so a program links only the families it * names: each runs its methods on a Buffer receiver and gives any other * receiver the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool read = !strncmp(method, "read", 4); const char *rest = method + (read ? 4 : 5); @@ -518,40 +519,45 @@ ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *meth if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_write(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_fill(recv, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) return scr_bytes_dyn_to_string(recv->v.bytes, args, argc); - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* indexOf, lastIndexOf and includes. */ -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { bool includes = !strcmp(method, "includes"); return scr_bytes_dyn_index_of(recv->v.bytes, args, argc, strcmp(method, "lastIndexOf") != 0, includes); } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } /* toJSON, equals, compare, copy, swap16/32/64, and the refused members. */ -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, - size_t argc, const char *what) { +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { if (scr_bytes_dyn_buffer(recv)) { ScrBytes *b = recv->v.bytes; if (!strcmp(method, "toJSON")) return scr_bytes_dyn_to_json(b); @@ -584,5 +590,5 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, return NULL; } } - return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f8dd9a0c0..f80c314b8 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v33(void) {} +void scr_runtime_abi_v35(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index d716f0de3..5633977b8 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v33(void); +void scr_runtime_abi_v35(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index cf50194c8..e0b33f539 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1296,15 +1296,19 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -/* scr_dyn_invoke_prepared for Buffer.prototype method names (scr_bytes_dyn.c), - * one entry per method family: a Buffer receiver runs the method, any other - * takes the ordinary dispatch. */ -ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for a call site whose method name is a constant: + * *mid_slot, the site's -1-initialized slot, caches the name's method id + * after the first call, so later calls skip the name lookup. */ +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared_id for Buffer.prototype method names + * (scr_bytes_dyn.c), one entry per method family: a Buffer receiver runs + * the method, any other takes the ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_write(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_fill(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_to_string(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer_search(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); +ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, int32_t *mid_slot, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 14d11dc67..c536d08b1 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -8,6 +8,7 @@ * scr_json.c; this unit uses only its public surface plus ScrJsonBuf. */ #include "scr_runtime.h" +#include "scr_dyn_methods.h" #include #include @@ -549,7 +550,11 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return scr_dyn_retain(scr_dyn_undefined()); } -static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; } +/* The dispatch below maps a call's method name to an id once + * (dyn_method_of, scr_dyn_methods.h) and its branches compare ids. Names + * compare by content, and any name the branches do not test is DYN_M_NONE. + * scripts/gen-dyn-methods.mjs writes the header from the DYN_M_ ids used + * here. */ /* Handle receivers: hand the whole call to the tag's ops (installed by * the owning unit at main() — a missing install is an internal error the @@ -692,29 +697,34 @@ static bool dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) { /* Names each prototype declares BEYOND what's implemented here — these * throw the "not supported yet" Error instead of mis-answering "is not a * function". */ -static bool dyn_arr_proto_unimpl(const char *m) { - static const char *names[] = { "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_unimpl(int m) { + return m == DYN_M_toString || m == DYN_M_toLocaleString; } -static bool dyn_arr_proto_mutates(const char *m) { - static const char *names[] = { - "push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL - }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_arr_proto_mutates(int m) { + switch (m) { + case DYN_M_push: case DYN_M_pop: case DYN_M_shift: case DYN_M_unshift: case DYN_M_reverse: + case DYN_M_sort: case DYN_M_fill: case DYN_M_copyWithin: case DYN_M_splice: + return true; + default: + return false; + } } -static bool dyn_bytes_proto_real(const char *m) { - static const char *names[] = { "slice", "at", "indexOf", "lastIndexOf", "includes", "join", - "forEach", "map", "filter", "some", "every", "find", "findIndex", "reverse", "fill", "set", - "subarray", "sort", "keys", "values", "entries", "reduce", "reduceRight", "copyWithin", - "toString", "toLocaleString", NULL }; - for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true; - return false; +static bool dyn_bytes_proto_real(int m) { + switch (m) { + case DYN_M_slice: case DYN_M_at: case DYN_M_indexOf: case DYN_M_lastIndexOf: case DYN_M_includes: + case DYN_M_join: case DYN_M_forEach: case DYN_M_map: case DYN_M_filter: case DYN_M_some: + case DYN_M_every: case DYN_M_find: case DYN_M_findIndex: case DYN_M_reverse: case DYN_M_fill: + case DYN_M_set: case DYN_M_subarray: case DYN_M_sort: case DYN_M_keys: case DYN_M_values: + case DYN_M_entries: case DYN_M_reduce: case DYN_M_reduceRight: case DYN_M_copyWithin: + case DYN_M_toString: case DYN_M_toLocaleString: + return true; + default: + return false; + } } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv); /* Live native arrays: the constant-time methods that loops call once per @@ -724,18 +734,18 @@ static ScrDyn *scr_dyn_invoke_impl( * array. Anything that could observe more — a hole, argument coercion, a * multi-value push, a value the element type rejects — sets *handled false * and takes the snapshot path. */ -static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args, +static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, const char *what, bool *handled) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(recv); if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; - if (dyn_name_is(method, "push") && argc == 1 && ops->set) { + if ((mid == DYN_M_push) && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) { *handled = true; return scr_dyn_retain(scr_dyn_undefined()); @@ -746,7 +756,7 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *c *handled = true; return last; } - if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { + if ((mid == DYN_M_at) && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) { double index = dyn_index_arg(args, argc, 0, 0, what); if (index < 0) index += (double)len; if (index < 0 || index >= (double)len) { @@ -786,11 +796,25 @@ ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - return scr_dyn_invoke_impl(recv, method, args, argc, what, NULL); + return scr_dyn_invoke_impl(recv, method, dyn_method_of(method), args, argc, what, NULL); +} + +ScrDyn *scr_dyn_invoke_prepared_id(ScrDyn *recv, ScrDyn *callee, const char *method, + int32_t *mid_slot, ScrDyn *const *args, size_t argc, + const char *what) { + if (callee) return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); + /* The slot is the call site's, shared by every thread: each writes the + * same id. */ + int32_t mid = __atomic_load_n(mid_slot, __ATOMIC_RELAXED); + if (mid < 0) { + mid = dyn_method_of(method); + __atomic_store_n(mid_slot, mid, __ATOMIC_RELAXED); + } + return scr_dyn_invoke_impl(recv, method, mid, args, argc, what, NULL); } static ScrDyn *scr_dyn_invoke_impl( - ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, + ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) { ScrJsonBuf b; @@ -811,7 +835,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_HANDLE) { return scr_dynh_dispatch(recv, method, args, argc, what); } - if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(method)) { + if (scr_dyn_is_array_prototype(recv) && dyn_arr_proto_mutates(mid)) { scr_dyn_array_prototype_element_fence(recv); return NULL; } @@ -836,7 +860,7 @@ static ScrDyn *scr_dyn_invoke_impl( * snapshot identity back to the externally visible capsule. */ if (recv->kind == SCR_DYN_TYPED_REF) { bool handled = false; - ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); + ScrDyn *fast = dyn_live_array_method(recv, mid, args, argc, what, &handled); if (handled) return fast; /* Iterator helpers on a compiled generator. */ if (scr_dyn_generator_helper_hook && @@ -861,9 +885,9 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } bool mutates = materialized->kind == SCR_DYN_ARR && - dyn_arr_proto_mutates(method); + dyn_arr_proto_mutates(mid); ScrDyn *result = scr_dyn_invoke_impl( - materialized, method, args, argc, what, recv); + materialized, method, mid, args, argc, what, recv); if (mutates && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); if (result == materialized) { scr_dyn_release(result); @@ -892,18 +916,18 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_FUNC) { - if (dyn_name_is(method, "apply")) { + if ((mid == DYN_M_apply)) { return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(), argc > 1 ? args[1] : scr_dyn_undefined(), what); } - if (dyn_name_is(method, "call")) { + if ((mid == DYN_M_call)) { scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined()); ScrDyn *r = scr_dyn_call(recv, argc > 1 ? args + 1 : NULL, argc > 1 ? argc - 1 : 0, what); scr_dyn_this_pop(); return r; } - if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc); - if (dyn_name_is(method, "toString")) { + if ((mid == DYN_M_bind)) return scr_dyn_bind(recv, args, argc); + if ((mid == DYN_M_toString)) { ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method)); if (member && member->kind != SCR_DYN_UNDEF) { scr_dyn_this_push_dyn(recv); @@ -938,9 +962,9 @@ static ScrDyn *scr_dyn_invoke_impl( } if ((recv->kind == SCR_DYN_SYMBOL || recv->kind == SCR_DYN_BIGINT || recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) && - dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + (mid == DYN_M_valueOf)) return scr_dyn_retain(recv); - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toFixed")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toFixed)) { double digits = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; ScrStr *text = scr_num_to_fixed(recv->v.num, digits); @@ -949,7 +973,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(text); return result; } - if (recv->kind == SCR_DYN_NUM && dyn_name_is(method, "toExponential")) { + if (recv->kind == SCR_DYN_NUM && (mid == DYN_M_toExponential)) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { dyn_throw_unsupported("Number", method); return NULL; @@ -962,7 +986,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (recv->kind == SCR_DYN_STR) { ScrStr *s = recv->v.str; - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double start = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double end = dyn_index_arg(args, argc, 1, scr_str_utf16_len(s), what); @@ -972,16 +996,16 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) && + if ((mid == DYN_M_at) || (mid == DYN_M_concat) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) && argc >= 1 && args[0]->kind == SCR_DYN_STR) { - double position = dyn_index_arg(args, argc, 1, dyn_name_is(method, "lastIndexOf") ? INFINITY : 0, what); + double position = dyn_index_arg(args, argc, 1, (mid == DYN_M_lastIndexOf) ? INFINITY : 0, what); if (scr_exc_pending()) return NULL; - if (dyn_name_is(method, "includes")) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); - if (dyn_name_is(method, "indexOf")) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); + if ((mid == DYN_M_includes)) return scr_dyn_new_bool(scr_str_index_of(s, args[0]->v.str, position) >= 0); + if ((mid == DYN_M_indexOf)) return scr_dyn_new_num(scr_str_index_of(s, args[0]->v.str, position)); return scr_dyn_new_num(scr_str_last_index_of_from(s, args[0]->v.str, position)); } dyn_throw_unsupported("String", method); @@ -992,19 +1016,19 @@ static ScrDyn *scr_dyn_invoke_impl( } if (recv->kind == SCR_DYN_ARR) { - if (dyn_name_is(method, "keys") || dyn_name_is(method, "values") || dyn_name_is(method, "entries")) + if ((mid == DYN_M_keys) || (mid == DYN_M_values) || (mid == DYN_M_entries)) return scr_dyn_native_handle_iterator(callback_recv ? callback_recv : recv, - dyn_name_is(method, "keys") ? 1 : dyn_name_is(method, "entries") ? 2 : 0); + (mid == DYN_M_keys) ? 1 : (mid == DYN_M_entries) ? 2 : 0); size_t len = recv->v.arr.len; if (recv->v.arr.presence) { bool sparse = false; for (size_t i = 0; i < len; i++) if (!recv->v.arr.presence[i]) { sparse = true; break; } if (!sparse) { free(recv->v.arr.presence); recv->v.arr.presence = NULL; } - else if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || dyn_name_is(method, "filter") || - dyn_name_is(method, "some") || dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex") || dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex") || - dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight") || dyn_name_is(method, "slice") || - dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { + else if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_reduce) || (mid == DYN_M_reduceRight) || (mid == DYN_M_slice) || + (mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes)) { ScrDyn *pack = scr_dyn_new_arr(); for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(pack, scr_dyn_retain(args[i])); ScrStr *name = scr_str_new(method, strlen(method)); @@ -1012,7 +1036,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(name); scr_dyn_release(pack); return result; - } else if (dyn_arr_proto_mutates(method) && !dyn_name_is(method, "fill") && !dyn_name_is(method, "push") && !dyn_name_is(method, "pop") && !dyn_name_is(method, "shift")) { + } else if (dyn_arr_proto_mutates(mid) && !(mid == DYN_M_fill) && !(mid == DYN_M_push) && !(mid == DYN_M_pop) && !(mid == DYN_M_shift)) { static const char message[] = "This checked array mutation over holes is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; @@ -1020,11 +1044,11 @@ static ScrDyn *scr_dyn_invoke_impl( } size_t splice_start = 0; size_t splice_removed = 0; - if (dyn_arr_proto_mutates(method)) { - bool grows = dyn_name_is(method, "push") && argc > 0; - grows = grows || (dyn_name_is(method, "unshift") && argc > 0); - bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0; - if (dyn_name_is(method, "splice")) { + if (dyn_arr_proto_mutates(mid)) { + bool grows = (mid == DYN_M_push) && argc > 0; + grows = grows || ((mid == DYN_M_unshift) && argc > 0); + bool shrinks = ((mid == DYN_M_pop) || (mid == DYN_M_shift)) && len > 0; + if ((mid == DYN_M_splice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; splice_start = dyn_rel_index(startD, len); @@ -1061,33 +1085,33 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && (dyn_name_is(method, "push") || - dyn_name_is(method, "pop") || dyn_name_is(method, "shift") || - dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) { + if (recv->v.arr.frozen && ((mid == DYN_M_push) || + (mid == DYN_M_pop) || (mid == DYN_M_shift) || + (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { static const char message[] = "Cannot assign to read only property 'length'"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) || - (recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) || - (dyn_name_is(method, "reverse") && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || + (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || + ((mid == DYN_M_reverse) && len > 1)))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } } - if (dyn_name_is(method, "push")) { + if ((mid == DYN_M_push)) { for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i])); return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "pop")) { + if ((mid == DYN_M_pop)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *result = scr_dyn_arr_at(recv, (double)(len - 1)); if (!result) return NULL; scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); return result; } - if (dyn_name_is(method, "shift")) { + if ((mid == DYN_M_shift)) { if (len == 0) return scr_dyn_retain(scr_dyn_undefined()); if (recv->v.arr.sealed) { if (recv->v.arr.frozen) { @@ -1109,7 +1133,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = len - 1; return first; /* ownership moves out */ } - if (dyn_name_is(method, "unshift")) { + if ((mid == DYN_M_unshift)) { /* Grow the backing store before shifting the old elements. The * appended placeholders are immortal, so overwriting them does not * abandon retained argument references. */ @@ -1121,7 +1145,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_new_num((double)recv->v.arr.len); } - if (dyn_name_is(method, "splice")) { + if ((mid == DYN_M_splice)) { size_t start = splice_start; size_t removed = splice_removed; size_t inserted = argc > 2 ? argc - 2 : 0; @@ -1151,7 +1175,7 @@ static ScrDyn *scr_dyn_invoke_impl( recv->v.arr.len = next_len; return out; } - if (dyn_name_is(method, "slice")) { + if ((mid == DYN_M_slice)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 1, (double)len, what); @@ -1163,7 +1187,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start; i < end; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i])); return out; } - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1171,17 +1195,17 @@ static ScrDyn *scr_dyn_invoke_impl( if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_arr_at(recv, idx); } - if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || - dyn_name_is(method, "includes")) { - if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + if ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || + (mid == DYN_M_includes)) { + if (len == 0) return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined(); - double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0; + double from = (mid == DYN_M_lastIndexOf) ? (double)len - 1 : 0; if (argc > 1) { from = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; } if (!dyn_arr_length_unchanged(recv, len)) return NULL; - if (dyn_name_is(method, "lastIndexOf")) { + if ((mid == DYN_M_lastIndexOf)) { double first = from < 0 ? (double)len + from : from; if (first >= (double)len) first = (double)len - 1; for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) { @@ -1193,16 +1217,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (first < 0) first = 0; for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) { const ScrDyn *item = recv->v.arr.items[i]; - bool nan_match = dyn_name_is(method, "includes") && + bool nan_match = (mid == DYN_M_includes) && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM && item->v.num != item->v.num && needle->v.num != needle->v.num; if (nan_match || scr_dyn_strict_eq(item, needle)) { - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i); } } - return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); + return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t i = len; i > 0; i--) { @@ -1215,16 +1239,16 @@ static ScrDyn *scr_dyn_invoke_impl( bool matched = scr_dyn_truthy(result); scr_dyn_release(result); if (matched) { - if (dyn_name_is(method, "findLast")) return item; + if ((mid == DYN_M_findLast)) return item; scr_dyn_release(item); return scr_dyn_new_num((double)index); } scr_dyn_release(item); } - return dyn_name_is(method, "findLast") + return (mid == DYN_M_findLast) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); } - if (dyn_name_is(method, "join")) { + if ((mid == DYN_M_join)) { ScrJsonBuf b; scr_jb_init(&b); ScrStr *separator = argc > 0 && args[0]->kind != SCR_DYN_UNDEF @@ -1250,7 +1274,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return r; } - if (dyn_name_is(method, "concat")) { + if ((mid == DYN_M_concat)) { ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { if (!dyn_arr_copy_item(out, recv, i, true)) { scr_dyn_release(out); return NULL; } @@ -1271,7 +1295,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "flat")) { + if ((mid == DYN_M_flat)) { double depth = dyn_index_arg(args, argc, 0, 1, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1283,7 +1307,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "reverse")) { + if ((mid == DYN_M_reverse)) { for (size_t i = 0; i < len / 2; i++) { ScrDyn *tmp = recv->v.arr.items[i]; recv->v.arr.items[i] = recv->v.arr.items[len - 1 - i]; @@ -1291,12 +1315,12 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toReversed")) { + if ((mid == DYN_M_toReversed)) { ScrDyn *out = scr_dyn_new_arr(); if (!dyn_arr_reversed_copy(out, recv)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "with")) { + if ((mid == DYN_M_with)) { double indexD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; if (!dyn_arr_length_unchanged(recv, len)) return NULL; @@ -1314,7 +1338,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "toSpliced")) { + if ((mid == DYN_M_toSpliced)) { double startD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; size_t start = dyn_rel_index(startD, len); @@ -1331,7 +1355,7 @@ static ScrDyn *scr_dyn_invoke_impl( for (size_t i = start + delete_count; i < len; i++) if (!dyn_arr_copy_item(out, recv, i, false)) { scr_dyn_release(out); return NULL; } return out; } - if (dyn_name_is(method, "fill")) { + if ((mid == DYN_M_fill)) { double startD = dyn_index_arg(args, argc, 1, 0, what); if (scr_exc_pending()) return NULL; double endD = dyn_index_arg(args, argc, 2, (double)len, what); @@ -1359,7 +1383,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double targetD = dyn_index_arg(args, argc, 0, 0, what); if (scr_exc_pending()) return NULL; double startD = dyn_index_arg(args, argc, 1, 0, what); @@ -1392,13 +1416,13 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") || - dyn_name_is(method, "filter") || dyn_name_is(method, "some") || - dyn_name_is(method, "every") || dyn_name_is(method, "find") || - dyn_name_is(method, "findIndex")) { + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { if (!dyn_cb_check(args, argc)) return NULL; ScrDyn *cb = args[0]; - ScrDyn *out = (dyn_name_is(method, "map") || dyn_name_is(method, "filter")) ? scr_dyn_new_arr() : NULL; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; /* Array iteration methods snapshot length before the first callback. * A shrinking callback can remove a later dense entry; an expanding * callback must not make the new tail observable to this pass. Live @@ -1413,30 +1437,30 @@ static ScrDyn *scr_dyn_invoke_impl( ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if (dyn_name_is(method, "map")) { + if ((mid == DYN_M_map)) { scr_dyn_arr_push(out, r); /* ownership moves in */ r = NULL; } else { bool truthy = scr_dyn_truthy(r); scr_dyn_release(r); - if (dyn_name_is(method, "filter") && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if (dyn_name_is(method, "some") && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if (dyn_name_is(method, "every") && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if (dyn_name_is(method, "find") && truthy) return item; /* +1 moves out */ - if (dyn_name_is(method, "findIndex") && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } } scr_dyn_release(item); } if (out) return out; - if (dyn_name_is(method, "some")) return scr_dyn_new_bool(false); - if (dyn_name_is(method, "every")) return scr_dyn_new_bool(true); - if (dyn_name_is(method, "find")) return scr_dyn_retain(scr_dyn_undefined()); - if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1); + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ } - if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) { + if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; - bool backward = dyn_name_is(method, "reduceRight"); + bool backward = (mid == DYN_M_reduceRight); ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL; ScrDyn *visible_recv = callback_recv ? callback_recv : recv; for (size_t step = 0; step < len; step++) { @@ -1458,7 +1482,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if (dyn_name_is(method, "flatMap")) { + if ((mid == DYN_M_flatMap)) { /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 * flatten. Native dyn-array results flatten element-by-element; a * WRAPPED engine array flattens through the routed keyed reads @@ -1496,7 +1520,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_name_is(method, "sort")) { + if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { /* V8 appends the received value's string image. */ @@ -1510,7 +1534,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL; return scr_dyn_retain(recv); } - if (dyn_name_is(method, "toSorted")) { + if ((mid == DYN_M_toSorted)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { static const char message[] = "The comparison function must be either a function or undefined"; @@ -1525,7 +1549,7 @@ static ScrDyn *scr_dyn_invoke_impl( } return out; } - if (dyn_arr_proto_unimpl(method)) { + if (dyn_arr_proto_unimpl(mid)) { dyn_throw_unsupported("Array", method); return NULL; } @@ -1541,14 +1565,14 @@ static ScrDyn *scr_dyn_invoke_impl( #ifdef SCR_LIB scr_trap("scriptc: promise dispatch is unavailable in library mode\n"); #else - if (dyn_name_is(method, "then")) { + if ((mid == DYN_M_then)) { return scr_dyn_promise_then(recv->v.promise, argc >= 1 ? args[0] : NULL, argc >= 2 ? args[1] : NULL, NULL); } - if (dyn_name_is(method, "catch")) { + if ((mid == DYN_M_catch)) { return scr_dyn_promise_then(recv->v.promise, NULL, argc >= 1 ? args[0] : NULL, NULL); } - if (dyn_name_is(method, "finally")) { + if ((mid == DYN_M_finally)) { return scr_dyn_promise_then(recv->v.promise, NULL, NULL, argc >= 1 ? args[0] : NULL); } dyn_throw_not_fn(what); @@ -1600,44 +1624,44 @@ static ScrDyn *scr_dyn_invoke_impl( } /* The typed array methods below are not DataView's; it inherits * Object.prototype's. */ - if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { + if ((mid == DYN_M_toString) || (mid == DYN_M_toLocaleString)) { ScrStr *tag = scr_str_new("[object DataView]", 17); ScrDyn *out = scr_dyn_new_str(tag); scr_str_release(tag); return out; } - if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + if ((mid == DYN_M_valueOf)) return scr_dyn_retain(recv); dyn_throw_not_fn(what); return NULL; } size_t blen = bytes->len; - if (dyn_name_is(method, "at")) { + if ((mid == DYN_M_at)) { double iD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double idx = iD < 0 ? (double)blen + iD : iD; if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); return scr_dyn_new_num(scr_bytes_get(bytes, idx)); } - if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { + if ((mid == DYN_M_slice) || (mid == DYN_M_subarray)) { /* subarray and Buffer.slice alias their source. TypedArray.slice * owns an independent copy; all keep the receiver's Buffer flavor. */ double startD = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen); if (scr_exc_pending()) return NULL; - ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") + ScrBytes *out = recv->buffer || (mid == DYN_M_subarray) ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ScrDyn *d = recv->buffer ? scr_dyn_new_buffer(out) : scr_dyn_new_bytes(out); scr_bytes_release(out); return d; } - if (dyn_name_is(method, "set")) { + if ((mid == DYN_M_set)) { double offset = dyn_bytes_index_arg(args, argc, 1, 0); if (scr_exc_pending()) return NULL; scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (dyn_name_is(method, "copyWithin")) { + if ((mid == DYN_M_copyWithin)) { double target = dyn_bytes_index_arg(args, argc, 0, 0); if (scr_exc_pending()) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1647,7 +1671,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "fill") && !recv->buffer) { + if ((mid == DYN_M_fill) && !recv->buffer) { double value; if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL; double start = dyn_bytes_index_arg(args, argc, 1, 0); @@ -1657,8 +1681,8 @@ static ScrDyn *scr_dyn_invoke_impl( scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end)); return scr_dyn_retain(recv); } - if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) { - ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF + if ((mid == DYN_M_join) || ((mid == DYN_M_toString) && !recv->buffer)) { + ScrStr *separator = (mid == DYN_M_join) && argc && args[0]->kind != SCR_DYN_UNDEF ? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1); if (scr_exc_pending()) { scr_str_release(separator); return NULL; } ScrStr *joined = scr_bytes_join(bytes, separator); @@ -1667,7 +1691,7 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } - if (dyn_bytes_proto_real(method)) { + if (dyn_bytes_proto_real(mid)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; } diff --git a/packages/runtime/src/scr_dyn_methods.h b/packages/runtime/src/scr_dyn_methods.h new file mode 100644 index 000000000..49e5c7254 --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.h @@ -0,0 +1,160 @@ +/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c + * uses. Do not edit by hand. */ +#ifndef SCR_DYN_METHODS_H +#define SCR_DYN_METHODS_H + +#include +#include + +enum { + DYN_M_NONE, + DYN_M_apply, + DYN_M_at, + DYN_M_bind, + DYN_M_call, + DYN_M_catch, + DYN_M_concat, + DYN_M_copyWithin, + DYN_M_entries, + DYN_M_every, + DYN_M_fill, + DYN_M_filter, + DYN_M_finally, + DYN_M_find, + DYN_M_findIndex, + DYN_M_findLast, + DYN_M_findLastIndex, + DYN_M_flat, + DYN_M_flatMap, + DYN_M_forEach, + DYN_M_includes, + DYN_M_indexOf, + DYN_M_join, + DYN_M_keys, + DYN_M_lastIndexOf, + DYN_M_map, + DYN_M_pop, + DYN_M_push, + DYN_M_reduce, + DYN_M_reduceRight, + DYN_M_reverse, + DYN_M_set, + DYN_M_shift, + DYN_M_slice, + DYN_M_some, + DYN_M_sort, + DYN_M_splice, + DYN_M_subarray, + DYN_M_then, + DYN_M_toExponential, + DYN_M_toFixed, + DYN_M_toLocaleString, + DYN_M_toReversed, + DYN_M_toSorted, + DYN_M_toSpliced, + DYN_M_toString, + DYN_M_unshift, + DYN_M_valueOf, + DYN_M_values, + DYN_M_with, + DYN_M_COUNT +}; + +static const char *const dyn_method_names[DYN_M_COUNT] = { + NULL, + "apply", + "at", + "bind", + "call", + "catch", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "finally", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "set", + "shift", + "slice", + "some", + "sort", + "splice", + "subarray", + "then", + "toExponential", + "toFixed", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "valueOf", + "values", + "with", +}; + +static const unsigned char dyn_method_lengths[DYN_M_COUNT] = { + 0, 5, 2, 4, 4, 5, 6, 10, 7, 5, 4, 6, 7, 4, 9, 8, + 13, 4, 7, 7, 8, 7, 4, 4, 11, 3, 3, 4, 6, 11, 7, 3, + 5, 5, 4, 4, 6, 8, 4, 13, 7, 14, 10, 8, 9, 8, 7, 7, + 6, 4, +}; + +/* Each name's id at its slot (or the next free one); 0 ends a probe. */ +static const unsigned char dyn_method_slots[256] = { + 0, 0, 0, 0, 12, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 14, 0, 0, 27, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 28, 3, 0, + 22, 0, 20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, + 0, 0, 0, 0, 0, 33, 31, 0, 0, 0, 0, 0, 36, 35, 0, 0, + 0, 0, 0, 0, 23, 4, 32, 0, 5, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 6, 0, 0, 37, 0, + 0, 40, 0, 0, 0, 0, 0, 0, 43, 0, 0, 45, 0, 0, 0, 44, + 0, 7, 0, 0, 0, 0, 42, 0, 25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, + 46, 0, 0, 39, 0, 41, 0, 9, 0, 0, 13, 0, 0, 0, 0, 8, + 0, 47, 10, 0, 0, 0, 0, 48, 0, 0, 17, 0, 0, 49, 0, 0, + 0, 0, 0, 19, 0, 0, 11, 0, 0, 0, 0, 18, 0, 0, 0, 0, +}; + +#define DYN_METHOD_MAX_LENGTH 14 + +/* The id of a method name, compared by content; DYN_M_NONE for any other + * name. */ +static inline int dyn_method_of(const char *m) { + size_t length = strlen(m); + if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE; + unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 + + (unsigned char)m[length - 1]) & 255; + for (;;) { + unsigned id = dyn_method_slots[slot]; + if (id == 0) return DYN_M_NONE; + if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id; + slot = (slot + 1) & 255; + } +} + +#endif diff --git a/packages/runtime/src/scr_dyn_methods.test.ts b/packages/runtime/src/scr_dyn_methods.test.ts new file mode 100644 index 000000000..77911aa2d --- /dev/null +++ b/packages/runtime/src/scr_dyn_methods.test.ts @@ -0,0 +1,10 @@ +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; +import { expect, test } from "vitest"; + +test("scr_dyn_methods.h lists every method id the checked-dynamic dispatch compares", () => { + const script = join(import.meta.dirname, "../../../scripts/gen-dyn-methods.mjs"); + expect(() => + execFileSync(process.execPath, [script, "--check"], { stdio: "pipe" }), + ).not.toThrow(); +}); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 3d10183f4..a23395c91 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -354,17 +354,23 @@ static bool scr_worker_transfer_has(const ScrDyn *transfer, const ScrWorkerView return false; } +/* Whether a method name is `name`: first characters compare inline, so + * the dispatch ladders below call strcmp only for names that can match. */ +static inline bool method_is(const char *method, const char *name) { + return method[0] == name[0] && strcmp(method, name) == 0; +} + static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "hasRef")) + if (view->port && method_is(method, "hasRef")) return scr_dyn_new_bool(view->referenced && (!view->closed || (view->channel && view->closing))); - if (!view->port && !strcmp(method, "terminate")) { + if (!view->port && method_is(method, "terminate")) { if (!view->termination) { view->termination = scr_promise_new(); if (view->closed) scr_promise_fulfill_ref(view->termination, @@ -373,7 +379,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_new_promise(view->termination); } - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { /* Worker drops messages after exit before validating or cloning. A * closed MessagePort still validates its payload, as Node does. */ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined()); @@ -405,9 +411,9 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho else scr_worker_thread_post(view->thread, message); return scr_dyn_retain(scr_dyn_undefined()); } - bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener"); - bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener"); - if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) { + bool once = method_is(method, "once") || method_is(method, "prependOnceListener"); + bool prepend = method_is(method, "prependListener") || method_is(method, "prependOnceListener"); + if (once || prepend || method_is(method, "on") || method_is(method, "addListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -416,7 +422,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "off") || !strcmp(method, "removeListener")) { + if (method_is(method, "off") || method_is(method, "removeListener")) { ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined(); scr_dyn_check_listener(listener, "listener"); if (scr_exc_pending()) return NULL; @@ -425,21 +431,21 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "removeAllListeners")) { + if (method_is(method, "removeAllListeners")) { bool all = !argc || args[0]->kind == SCR_DYN_UNDEF; if (!all && args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all)); return scr_exc_pending() ? NULL : scr_dyn_retain(self); } - if (!strcmp(method, "listenerCount")) { + if (method_is(method, "listenerCount")) { if (!argc || args[0]->kind != SCR_DYN_STR) return scr_worker_refusal("Worker event names must be strings"); return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1]) : scr_emitter_listener_count(view->events, args[0]->v.str)); } - if (view->port && !strcmp(method, "close")) { + if (view->port && method_is(method, "close")) { if (argc && args[0]->kind != SCR_DYN_UNDEF) { scr_dyn_check_listener(args[0], "callback"); if (scr_exc_pending()) return NULL; @@ -460,7 +466,7 @@ static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *metho } return scr_dyn_retain(scr_dyn_undefined()); } - if (view->port && !strcmp(method, "start")) { + if (view->port && method_is(method, "start")) { view->started = true; return scr_dyn_retain(scr_dyn_undefined()); } @@ -1021,7 +1027,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)what; ScrWorkerView *view = ptr; - if (!strcmp(method, "postMessage")) { + if (method_is(method, "postMessage")) { if (view->closed) { scr_throw_domex("InvalidStateError", "BroadcastChannel is closed."); return NULL; @@ -1045,7 +1051,7 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, scr_message_free(first); return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "close")) { + if (method_is(method, "close")) { if (!view->closed) { view->closed = true; scr_broadcast_leave(view->channel); @@ -1053,8 +1059,8 @@ static ScrDyn *scr_broadcast_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (!strcmp(method, "ref") || !strcmp(method, "unref")) { - view->referenced = !strcmp(method, "ref"); + if (method_is(method, "ref") || method_is(method, "unref")) { + view->referenced = method_is(method, "ref"); return scr_dyn_retain(self); } ScrDyn *out = NULL; diff --git a/scripts/gen-dyn-methods.mjs b/scripts/gen-dyn-methods.mjs new file mode 100644 index 000000000..fe4f81a90 --- /dev/null +++ b/scripts/gen-dyn-methods.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/* Writes packages/runtime/src/scr_dyn_methods.h: the method-name ids the + * checked-dynamic dispatch in scr_dyn_invoke.c compares (DYN_M_), the + * names, and an open-addressed hash table mapping a name to its id. The ids + * are the DYN_M_ names scr_dyn_invoke.c uses, so the header follows the + * dispatch code. + * + * node scripts/gen-dyn-methods.mjs write the header + * node scripts/gen-dyn-methods.mjs --check fail when it is out of date */ +import { readFileSync, writeFileSync } from "node:fs"; + +const source = new URL("../packages/runtime/src/scr_dyn_invoke.c", import.meta.url); +const target = new URL("../packages/runtime/src/scr_dyn_methods.h", import.meta.url); +const SLOTS = 256; + +const used = new Set(); +for (const match of readFileSync(source, "utf8").matchAll(/\bDYN_M_([A-Za-z][A-Za-z0-9]*)\b/g)) + if (match[1] !== "NONE" && match[1] !== "COUNT") used.add(match[1]); +const names = [...used].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +if (names.length >= SLOTS / 2) throw new Error("too many method names for the hash table"); + +// The slot dyn_method_of in the header computes. +const slotOf = (name) => + (name.length * 7 + name.charCodeAt(0) * 31 + name.charCodeAt(name.length - 1)) & (SLOTS - 1); +const slots = new Array(SLOTS).fill(0); +names.forEach((name, i) => { + let slot = slotOf(name); + while (slots[slot] !== 0) slot = (slot + 1) & (SLOTS - 1); + slots[slot] = i + 1; +}); + +const rows = (values, width) => { + const out = []; + for (let i = 0; i < values.length; i += width) + out.push(` ${values.slice(i, i + width).join(", ")},`); + return out; +}; +const text = [ + "/* Generated by scripts/gen-dyn-methods.mjs from the DYN_M_ ids scr_dyn_invoke.c", + " * uses. Do not edit by hand. */", + "#ifndef SCR_DYN_METHODS_H", + "#define SCR_DYN_METHODS_H", + "", + "#include ", + "#include ", + "", + "enum {", + " DYN_M_NONE,", + ...names.map((name) => ` DYN_M_${name},`), + " DYN_M_COUNT", + "};", + "", + "static const char *const dyn_method_names[DYN_M_COUNT] = {", + " NULL,", + ...names.map((name) => ` ${JSON.stringify(name)},`), + "};", + "", + "static const unsigned char dyn_method_lengths[DYN_M_COUNT] = {", + ...rows([0, ...names.map((name) => name.length)], 16), + "};", + "", + `/* Each name's id at its slot (or the next free one); 0 ends a probe. */`, + `static const unsigned char dyn_method_slots[${SLOTS}] = {`, + ...rows(slots, 16), + "};", + "", + `#define DYN_METHOD_MAX_LENGTH ${Math.max(...names.map((name) => name.length))}`, + "", + "/* The id of a method name, compared by content; DYN_M_NONE for any other", + " * name. */", + "static inline int dyn_method_of(const char *m) {", + " size_t length = strlen(m);", + " if (length == 0 || length > DYN_METHOD_MAX_LENGTH) return DYN_M_NONE;", + " unsigned slot = (unsigned)(length * 7 + (unsigned char)m[0] * 31 +", + ` (unsigned char)m[length - 1]) & ${SLOTS - 1};`, + " for (;;) {", + " unsigned id = dyn_method_slots[slot];", + " if (id == 0) return DYN_M_NONE;", + " if (dyn_method_lengths[id] == length && memcmp(m, dyn_method_names[id], length) == 0) return (int)id;", + ` slot = (slot + 1) & ${SLOTS - 1};`, + " }", + "}", + "", + "#endif", + "", +].join("\n"); + +if (process.argv.includes("--check")) { + let current = ""; + try { + current = readFileSync(target, "utf8"); + } catch {} + if (current !== text) { + console.error( + "packages/runtime/src/scr_dyn_methods.h is out of date: run node scripts/gen-dyn-methods.mjs", + ); + process.exit(1); + } +} else { + writeFileSync(target, text); +} diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 3158de997..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,7 +394,8 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/dynamic-methods is + * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -408,6 +409,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/intern-table", "bench/thread-slots", "bench/channel-messages", + "bench/dynamic-methods", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 4d5546ab6..85fa73410 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 35, marker: "scr_runtime_abi_v35" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 4a17b3f461b5223eca5e29897164d5a287991f4b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:05:57 -0500 Subject: [PATCH 27/30] Keep the dynamic call for observed methods called with a spread argument --- .../src/frontend/lowering/lower-calls.ts | 5 ++- .../test/observed-method-guard.test.ts | 42 +++++++++++++++++++ .../main.ts | 28 +++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 493d979f5..fd26a832b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10198,10 +10198,13 @@ export function lowerObjectMethodCall( // The declaring class's original value, built with the observed slot. // An override below (a virtual call) keeps the dynamic call: a slot // reassigned to an ancestor's method must not reach the override. So do - // methods declared in unchecked JavaScript, whose results are inferred. + // methods declared in unchecked JavaScript, whose results are inferred, + // and calls with a spread argument: the dynamic call drains the spread + // through the iterator's current `next`, which may be a replacement. const declarerFile = found.declarer.decl ? ts.getSourceFileOfNode(found.declarer.decl) : null; const original = virtual || + call.arguments.nodes.some(ts.isSpreadElement) || !declarerFile || (isJsSourceFile(declarerFile) && !lowerer.checkedJsFile(declarerFile)) ? undefined diff --git a/packages/compiler/test/observed-method-guard.test.ts b/packages/compiler/test/observed-method-guard.test.ts index 5964c482b..cafe872f9 100644 --- a/packages/compiler/test/observed-method-guard.test.ts +++ b/packages/compiler/test/observed-method-guard.test.ts @@ -75,3 +75,45 @@ test("typed calls of an observed method call it natively while its slot is uncha true, ); }); + +// A spread argument keeps the dynamic call, which drains the spread through +// the iterator's current `next` (a replacement included). +test("typed calls of an observed method with a spread argument keep the dynamic call", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-observed-guard-")); + dirs.push(dir); + const entry = join(dir, "main.ts"); + const outDir = join(dir, ".scriptc"); + const outPath = join(outDir, "main.ir.json"); + await writeFile( + join(dir, "untyped.js"), + "export function put(target, key, value) { target[key] = value; }\n", + ); + await writeFile( + entry, + [ + 'import { put } from "./untyped.js";', + "class Bag {", + " sum(...values: number[]): number { let s = 0; for (const n of values) s += n; return s; }", + "}", + "const bag = new Bag();", + "put({}, String(Math.random()), 1);", + "export function spread(b: Bag, values: number[]): number { return b.sum(...values); }", + "console.log(spread(bag, [1, 2]));", + "", + ].join("\n"), + ); + const result = await compile(entry, { outDir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const module = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(module)).toEqual([]); + const spread = module.functions.find((fn) => fn.name.endsWith("spread")); + expect(spread).toBeDefined(); + const guards = recordsOf(spread).filter( + (record) => + record.kind === "ternary" && + (record.cond as IrRecord | undefined)?.kind === "libCall" && + (record.cond as IrRecord).fn === "dyn.sameValue", + ); + expect(guards).toHaveLength(0); +}); diff --git a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts index f7449f1af..c5475b5d9 100644 --- a/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts +++ b/tests/corpus/7365-class-methods-untyped-computed-writes/main.ts @@ -57,3 +57,31 @@ patch(proto, "list", () => ["patched"]); console.log(repo.list().join(",")); patch(proto, "list", saved); console.log(repo.list().join(",")); + +// a spread argument drains its iterator through the iterator's current `next` +class Bag { + sum(...values: number[]): number { + let total = 0; + for (const value of values) total += value; + return total; + } +} +class Countdown { + private left: number; + constructor(left: number) { + this.left = left; + } + next(): IteratorResult { + return this.left > 0 ? { done: false, value: this.left-- } : { done: true, value: undefined }; + } + [Symbol.iterator](): Countdown { + return this; + } +} +const bag = new Bag(); +console.log(bag.sum(...[1, 2, 3]), bag.sum(...new Countdown(3))); +const replaced = new Countdown(3); +patch(replaced, "next", function (): IteratorResult { + return { done: true, value: undefined }; +}); +console.log(bag.sum(...replaced)); From 257d4dc5cb9f96e6eb097c342d0acd99942cfb28 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:11:30 -0500 Subject: [PATCH 28/30] Declare flock on macOS in the native compiler host host.c defines _POSIX_C_SOURCE, under which Darwin's hides flock and LOCK_EX/LOCK_UN, so the native compiler build failed on macOS with an implicit declaration error. Define _DARWIN_C_SOURCE on Apple targets before the system headers. Linux preprocessing is unchanged. --- packages/compiler/native/host.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/compiler/native/host.c b/packages/compiler/native/host.c index eeead7eff..f54bdaa3f 100644 --- a/packages/compiler/native/host.c +++ b/packages/compiler/native/host.c @@ -1,6 +1,10 @@ #ifndef _WIN32 #define _POSIX_C_SOURCE 200809L #endif +/* flock and LOCK_EX/LOCK_UN, which Darwin hides under _POSIX_C_SOURCE. */ +#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) +#define _DARWIN_C_SOURCE 1 +#endif #include #include #include From cba3fa6a05ff6c2a00741671997730605aaf45bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:25:59 -0500 Subject: [PATCH 29/30] Keep projected files valid for bare async arrows and unbraced bodies, and adopt tuple elements - A bare arrow parameter after `async` (`async x => ...`) gets parentheses with its projected type, as `x => ...` does. Without them the rewritten file failed its check and lost its projection (SC6102). - A cast that starts an unbraced loop or `if` body gets no `;` guard. Only a statement in a statement list can continue the previous line, and in a body the `;` became the body, which moved the statement after the loop. - Adoption walks a tuple's elements through the live element capsules of the tuple's view, so references package code holds to them see the native values. - scr_runtime.h declares scr_dyn_decl_adopt (in scr_dyn.h), which the backend emits, so llvm-runtime-abi.test.ts finds its prototype. - npm.test.ts gives zod3-adoption and zod3-boundary the long per-case timeout of the other zod 3 cases; their sanitized builds compile zod's types module too. --- .../compiler/src/frontend/decl-projection.ts | 20 +++++++++-- packages/runtime/src/scr_decl_adopt.c | 27 ++++++++++++++- packages/runtime/src/scr_dyn.h | 6 ++++ .../fixtures/npm/cases/zod3-adoption/main.ts | 9 +++++ .../npm/cases/zod3-boundary/rewrites.ts | 21 ++++++++++++ tests/harness/decl-boundary.test.ts | 33 ++++++++++++++++++- tests/harness/npm.test.ts | 9 +++-- 7 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/npm/cases/zod3-boundary/rewrites.ts diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 4ff843c8b..31d32834e 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -605,11 +605,15 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const name = jsNode.name; const nameStart = ts.tokenStartOfNode(name); const text = jsSf.text; + // `x => ...` or `async x => ...`: nothing but the modifiers comes + // before the parameter, which then needs parentheses for its type. + const head = ts.isArrowFunction(fn) + ? ((ts.getModifiers(fn) ?? []).at(-1)?.end ?? ts.tokenStartOfNode(fn)) + : nameStart; const bareArrow = ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && - text[ts.tokenStartOfNode(fn)] !== "(" && - text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === ""; + text.slice(head, nameStart).trim() === ""; const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end; if (bareArrow) { fileEdits.push({ start: nameStart, end: nameStart, text: "(" }); @@ -727,10 +731,20 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // semicolon continues the previous expression: guard it. let up: ts.Node | undefined = e; while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent; + // Only a statement in a statement list can follow such a line; an + // unbraced body (`for (...) S.parse(x).run()`) follows its header, + // and a `;` there would become the body. + const list = up?.parent?.parent; if ( up?.parent !== undefined && ts.isExpressionStatement(up.parent) && - ts.tokenStartOfNode(up.parent) === start + ts.tokenStartOfNode(up.parent) === start && + list !== undefined && + (ts.isBlock(list) || + ts.isSourceFile(list) || + ts.isModuleBlock(list) || + ts.isCaseClause(list) || + ts.isDefaultClause(list)) ) prefix = ";("; void stmt; diff --git a/packages/runtime/src/scr_decl_adopt.c b/packages/runtime/src/scr_decl_adopt.c index 3173ffb84..7372826ab 100644 --- a/packages/runtime/src/scr_decl_adopt.c +++ b/packages/runtime/src/scr_decl_adopt.c @@ -22,7 +22,6 @@ #include void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule); -void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); typedef struct { ScrDyn *node; @@ -120,6 +119,32 @@ static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsu return false; } } + } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && !capsule->v.typed_ref.array && + n->kind == SCR_DYN_ARR && !n->v.arr.presence) { + /* A capsule without element hooks over an array: a tuple, whose index + * field names get no record hooks. Its view holds a live capsule of + * each record or array element, which the source element adopts (the + * walk skips the other elements). */ + ScrDyn *view = capsule->v.typed_ref.materialize(ptr); + if (scr_exc_pending()) { + scr_dyn_release(view); + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + if (view && view->kind == SCR_DYN_ARR) { + size_t len = n->v.arr.len < view->v.arr.len ? n->v.arr.len : view->v.arr.len; + for (size_t i = 0; i < len; i++) { + ScrDyn *child = n->v.arr.items[i], *element = view->v.arr.items[i]; + if (!child || !element || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue; + ScrDynPath p = { path, NULL, i }; + if (!scr_decl_adopt_walk(list, child, scr_dyn_retain(element), &p, depth + 1)) { + scr_dyn_release(view); + if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule); + return false; + } + } + } + scr_dyn_release(view); } else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass && capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) { const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array; diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index cf50194c8..e18719ec1 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -368,6 +368,12 @@ typedef struct ScrDynTypedRecordOps { /* A record capsule's ops while it answers from the native record (no view * created yet), or NULL. */ const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d); +/* The declaration-typed boundary's adoption (scr_decl_adopt.c, linked by + * programs with boundary casts): the nodes of `source` that something else + * still references become live capsules of the native value `capsule` + * holds. Both borrowed; a node that is not plain data throws the path + * TypeError before anything changes. */ +void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule); /* Prototype members of a compiled class seen as `unknown`, emitted per * class by the compiler: the string names of methods, getters and setters diff --git a/tests/fixtures/npm/cases/zod3-adoption/main.ts b/tests/fixtures/npm/cases/zod3-adoption/main.ts index 6566d0e3a..d21ff69ee 100644 --- a/tests/fixtures/npm/cases/zod3-adoption/main.ts +++ b/tests/fixtures/npm/cases/zod3-adoption/main.ts @@ -37,3 +37,12 @@ cyc.meta = cyc; const back = AsRow.parse(cyc); back.id = 10; console.log("cycle", cyc.id, back === cyc, back.meta === cyc); +// A tuple's elements keep their identity too: the package-side array +// still holds the element that typed code mutates. +const Pair = z.custom<[{ id: number; tags: string[] }, string]>(); +const pairSource = JSON.parse('[{"id":1,"tags":["a"]},"s"]') as unknown[]; +const pairHead = pairSource[0]; +const pair = Pair.parse(pairSource); +pair[0].tags.push("b"); +pair[0].id = 2; +console.log("tuple", JSON.stringify(pairHead), JSON.stringify(pairSource), pair[1]); diff --git a/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts b/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts new file mode 100644 index 000000000..7a98cccf0 --- /dev/null +++ b/tests/fixtures/npm/cases/zod3-boundary/rewrites.ts @@ -0,0 +1,21 @@ +// Rewrites the declaration-typed boundary makes in user code: a typed +// parameter of a bare `async` arrow zod calls, and a cast at the start of +// an unbraced loop body. +import { z } from "zod"; + +const Box = z.object({ n: z.number() }); +const raw: unknown = { n: 2 }; + +// The cast opens the loop body's statement; the loop runs it each time. +let seen = 0; +for (let i = 0; i < 3; i++) Box.parse(raw).n > 0 && seen++; +let left = 2; +while (left-- > 0) Box.parse(raw).n > 1 && seen++; +console.log("loops", seen); + +// zod calls the callback with the string it checked. +const Length = z.string().transform(async s => s.length + 1); +async function main(): Promise { + console.log("async arrow", await Length.parseAsync("abc")); +} +void main(); diff --git a/tests/harness/decl-boundary.test.ts b/tests/harness/decl-boundary.test.ts index 6729dd530..931208836 100644 --- a/tests/harness/decl-boundary.test.ts +++ b/tests/harness/decl-boundary.test.ts @@ -3,7 +3,10 @@ * naming the path, and a value the native record cannot represent (a * frozen object) fails with the reason. Node does not check, so this is a * native-only test; npm.test.ts runs the differential cases - * (zod3-boundary, zod3-adoption, zod3-infer). */ + * (zod3-boundary, zod3-adoption, zod3-infer). The rewrite test checks that + * a file keeps its projection (no SC6102 note) where the rewrite has to + * parenthesize a bare async arrow's parameter and cast the start of an + * unbraced loop body. */ import { execFile } from "node:child_process"; import { mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -35,3 +38,31 @@ test( ); }, ); + +test( + "boundary rewrites keep bare async arrows and unbraced loop bodies valid", + { timeout: 600_000 }, + async () => { + const rewrites = join(repoRoot, "tests/fixtures/npm/cases/zod3-boundary/rewrites.ts"); + mkdirSync(outDir, { recursive: true }); + const result = await compile(rewrites, { + outPath: join(outDir, "rewrites"), + outDir, + sanitize, + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + // The file keeps its projection: no SC6102 note drops it (a build + // that lowered nothing carries no warnings at all). + expect((result.warnings ?? []).map((w) => w.code)).not.toContain("SC6102"); + const pending = execFileAsync(result.binaryPath, [], { + cwd: dirname(rewrites), + encoding: "utf8", + }); + pending.child.stdin?.end(); + const { stdout } = await pending; + // Node's output: every loop iteration runs the cast statement. + expect(stdout).toBe("loops 5\nasync arrow 4\n"); + }, +); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 9a13c0968..461b7fbcd 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -112,13 +112,16 @@ const KNOWN: Record = { const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); /** Per-case timeouts past the default 120 s: the undici cases compile the - * built-in undici implementation's module graph, and zod3 and zod3-infer - * compile zod 3's 3,700-line types module. A sanitized build of either - * takes minutes on a CI runner. */ + * built-in undici implementation's module graph, and the zod 3 cases + * (zod3, zod3-adoption, zod3-boundary, zod3-infer) compile zod 3's + * 3,700-line types module. A sanitized build of either takes minutes on a + * CI runner. */ const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000, zod3: 600_000, + "zod3-adoption": 600_000, + "zod3-boundary": 600_000, "zod3-infer": 600_000, }; From 108df00b295e8ce45f56cda16d6614b889987fc7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 00:37:49 -0500 Subject: [PATCH 30/30] Keep wrapped array literals native in typed global views A fresh array literal wrapped in parentheses or `satisfies` on the right side of a write to a member of a typed view of the global object (`g.cache ??= ([])`, `g.cache ||= [] satisfies number[]`) is now built as the declared native array too, so typed reads return the same array. globalViewSlot walks up through those wrappers to the write's right operand and answers it, and the slot type comes from that operand's contextual type. --- .../compiler/src/frontend/lowering/lowerer.ts | 37 ++++++++++++------- tests/corpus/7366-typed-global-view-arrays.ts | 16 ++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 554b703e9..4f560d807 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10607,25 +10607,31 @@ export class Lowerer { return this.coerceInto(node, value, expected); } - /** Lower for a known destination, then apply ordinary value coercion. - * Fresh arrays and records use the destination layout directly. */ - /** `value` is the right side of a write to a member of a typed view of the - * global object (`(globalThis as typeof globalThis & { k?: T }).k = value`, - * also through `??=` and `||=`). */ - private globalViewSlot(value: ts.Expression): boolean { - const write = value.parent; + /** The right side of a write to a member of a typed view of the global + * object (`(globalThis as typeof globalThis & { k?: T }).k = value`, also + * through `??=` and `||=`) that is `value` itself or wraps only it in + * parentheses or `satisfies`; null when `value` is not in such a write. */ + private globalViewSlot(value: ts.Expression): ts.Expression | null { + let operand: ts.Expression = value; + while ( + operand.parent !== undefined && + (ts.isParenthesizedExpression(operand.parent) || ts.isSatisfiesExpression(operand.parent)) + ) + operand = operand.parent; + const write = operand.parent; if ( + write === undefined || !ts.isBinaryExpression(write) || - write.right !== value || + write.right !== operand || (write.operatorToken.kind !== ts.SyntaxKind.EqualsToken && write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) ) - return false; + return null; let target = write.left; while (ts.isParenthesizedExpression(target)) target = target.expression; if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) - return false; + return null; const receiver = this.typeOf(target.expression); const isGlobal = (type: ts.Type): boolean => { const symbol = type.getSymbol(); @@ -10635,12 +10641,14 @@ export class Lowerer { symbol.declarations.length === 0 ); }; - return ( - isGlobal(receiver) || + return isGlobal(receiver) || (receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal)) - ); + ? operand + : null; } + /** Lower for a known destination, then apply ordinary value coercion. + * Fresh arrays and records use the destination layout directly. */ lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr { // These wrappers do not change the value or its inferred type. Keep a // fresh literal visible to destination-directed construction. Assertions @@ -10712,7 +10720,8 @@ export class Lowerer { // the native array and store it boxed: every typed read then extracts // that same array (a dynamic literal would be copied on each read, // losing writes such as push). - const slotType = this.globalViewSlot(x) ? this.checker.getContextualType(x) : undefined; + const slot = this.globalViewSlot(x); + const slotType = slot ? this.checker.getContextualType(slot) : undefined; const declared = slotType && this.checker.getNonNullableType(slotType); const native = declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) diff --git a/tests/corpus/7366-typed-global-view-arrays.ts b/tests/corpus/7366-typed-global-view-arrays.ts index 3b24620d5..e2d9b23ff 100644 --- a/tests/corpus/7366-typed-global-view-arrays.ts +++ b/tests/corpus/7366-typed-global-view-arrays.ts @@ -30,3 +30,19 @@ const counts = globalThis as typeof globalThis & { __exampleCounts?: number[][] counts.__exampleCounts = [[1], [2, 3]]; counts.__exampleCounts[1]!.push(4); console.log(JSON.stringify(counts.__exampleCounts)); + +// a parenthesized or `satisfies`-wrapped literal is kept the same way +type Wrapped = typeof globalThis & { __exampleWrapped?: number[]; __exampleSatisfied?: number[] }; +function wrapped(): number[] { + const g = globalThis as Wrapped; + g.__exampleWrapped ??= ([5]); + return g.__exampleWrapped; +} +function satisfied(): number[] { + const g = globalThis as Wrapped; + g.__exampleSatisfied ||= [7] satisfies number[]; + return g.__exampleSatisfied; +} +wrapped().push(6); +satisfied().push(8); +console.log(wrapped().join(","), wrapped() === wrapped(), satisfied().join(","), satisfied() === satisfied());