diff --git a/.changeset/modern-forks-raise.md b/.changeset/modern-forks-raise.md new file mode 100644 index 000000000..4cd5fdcb6 --- /dev/null +++ b/.changeset/modern-forks-raise.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +memoize the Parser per AST diff --git a/src/Parser.ts b/src/Parser.ts index ba18905b4..177f43527 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,6 +5,7 @@ import * as Effect from "effect/Effect" import * as Either from "effect/Either" import { pipe } from "effect/Function" +import { globalValue } from "effect/GlobalValue" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as ReadonlyArray from "effect/ReadonlyArray" @@ -18,7 +19,7 @@ const getEither = ( ast: AST.AST, isDecoding: boolean ): (i: unknown, options?: AST.ParseOptions) => Either.Either => - go(ast, isDecoding) as any + goMemo(ast, isDecoding) as any const getSync = (ast: AST.AST, isDecoding: boolean) => { const parser = getEither(ast, isDecoding) @@ -38,7 +39,7 @@ const getOption = (ast: AST.AST, isDecoding: boolean) => { } const getEffect = (ast: AST.AST, isDecoding: boolean) => { - const parser = go(ast, isDecoding) + const parser = goMemo(ast, isDecoding) return (input: unknown, options?: AST.ParseOptions) => parser(input, { ...options, isEffectAllowed: true }) } @@ -248,11 +249,31 @@ interface Parser { */ export const defaultParseOption: AST.ParseOptions = {} +const decodeMemoMap = globalValue( + Symbol.for("@effect/schema/Parser/decodeMemoMap"), + () => new WeakMap>() +) +const encodeMemoMap = globalValue( + Symbol.for("@effect/schema/Parser/encodeMemoMap"), + () => new WeakMap>() +) + +const goMemo = (ast: AST.AST, isDecoding: boolean): Parser => { + const memoMap = isDecoding ? decodeMemoMap : encodeMemoMap + const memo = memoMap.get(ast) + if (memo) { + return memo + } + const parser = go(ast, isDecoding) + memoMap.set(ast, parser) + return parser +} + const go = (ast: AST.AST, isDecoding: boolean): Parser => { switch (ast._tag) { case "Refinement": { if (isDecoding) { - const from = go(ast.from, true) + const from = goMemo(ast.from, true) return (i, options) => handleForbidden( ParseResult.flatMap( @@ -269,8 +290,8 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { options ) } else { - const from = go(AST.to(ast), true) - const to = go(dropRightRefinement(ast.from), false) + const from = goMemo(AST.to(ast), true) + const to = goMemo(dropRightRefinement(ast.from), false) return ( i, options @@ -279,8 +300,8 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { } case "Transform": { const transform = getFinalTransformation(ast.transformation, isDecoding) - const from = isDecoding ? go(ast.from, true) : go(ast.to, false) - const to = isDecoding ? go(ast.to, true) : go(ast.from, false) + const from = isDecoding ? goMemo(ast.from, true) : goMemo(ast.to, false) + const to = isDecoding ? goMemo(ast.to, true) : goMemo(ast.from, false) return (i1, options) => handleForbidden( ParseResult.flatMap( @@ -330,10 +351,10 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { return fromRefinement(ast, (u): u is any => Predicate.isString(u) && regex.test(u)) } case "Tuple": { - const elements = ast.elements.map((e) => go(e.type, isDecoding)) + const elements = ast.elements.map((e) => goMemo(e.type, isDecoding)) const rest = pipe( ast.rest, - Option.map(ReadonlyArray.mapNonEmpty((ast) => go(ast, isDecoding))) + Option.map(ReadonlyArray.mapNonEmpty((ast) => goMemo(ast, isDecoding))) ) let requiredLen = ast.elements.filter((e) => !e.isOptional).length if (Option.isSome(ast.rest)) { @@ -562,7 +583,7 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { const propertySignatures: Array> = [] const expectedKeys: Record = {} for (const ps of ast.propertySignatures) { - propertySignatures.push(go(ps.type, isDecoding)) + propertySignatures.push(goMemo(ps.type, isDecoding)) expectedKeys[ps.name] = null } @@ -573,8 +594,8 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { } = {} for (const is of ast.indexSignatures) { indexSignatures.push([ - go(is.parameter, isDecoding), - go(is.type, isDecoding) + goMemo(is.parameter, isDecoding), + goMemo(is.type, isDecoding) ]) const base = AST.getParameterBase(is.parameter) if (AST.isSymbolKeyword(base)) { @@ -792,7 +813,7 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { const len = ownKeys.length const map = new Map>() for (let i = 0; i < ast.types.length; i++) { - map.set(ast.types[i], go(ast.types[i], isDecoding)) + map.set(ast.types[i], goMemo(ast.types[i], isDecoding)) } return (input, options) => { const es: Array<[number, ParseResult.ParseErrors]> = [] @@ -909,7 +930,7 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => { } } case "Lazy": { - const get = Internal.memoizeThunk(() => go(ast.f(), isDecoding)) + const get = Internal.memoizeThunk(() => goMemo(ast.f(), isDecoding)) return (a, options) => get()(a, options) } }