1import * as Effect from "../../Effect.ts"
2import type { CompiledDecoder, Decode, Is, Make } from "../../schema/SchemaCompiler.ts"
3import type * as SchemaAST from "../../SchemaAST.ts"
4import type { Parser } from "../../SchemaParser.ts"
5import * as Interpreter from "./interpreter.ts"
6import * as InternalParser from "./parser.ts"
7
8/** @internal */
9export const invalid = Symbol()
10
11/** @internal */
12export type Resolve = (ast: SchemaAST.AST) => Entry
13
14/** @internal */
15export type DecoderSource = Partial<CompiledDecoder>
16
17/** @internal */
18export type DecoderOperation = keyof CompiledDecoder
19
20/** @internal */
21export type Compile = <K extends DecoderOperation>(
22 ast: SchemaAST.AST,
23 resolve: Resolve,
24 operation: K
25) => CompiledDecoder[K] | undefined
26
27/** @internal */
28export type CompileSource = (ast: SchemaAST.AST, resolve: Resolve) => DecoderSource | undefined
29
30/** @internal */
31export type Compiled = DecoderSource | Compile
32
33const cache = new WeakMap<SchemaAST.AST, Entry>()
34let compiler: CompileSource | undefined
35
36/** @internal */
37export let compilerAdaptersEnabled = false
38
39function activateCompilerAdapters(): void {
40 compilerAdaptersEnabled = true
41}
42
43const decodeChild = (ast: SchemaAST.AST): Parser =>
44 compilerAdaptersEnabled
45 ? lazyParser(resolve, ast, "parser")
46 : resolve(ast).parser
47const makeChild = (ast: SchemaAST.AST): Parser =>
48 compilerAdaptersEnabled
49 ? lazyParser(resolve, ast, "makeEffect")
50 : resolve(ast).makeEffect
51const makeField = (ast: SchemaAST.AST): Parser => Interpreter.compileField(ast, makeChild)
52
53/** @internal */
54export interface Entry {
55 readonly ast: SchemaAST.AST
56 readonly compiled?: Compiled | undefined
57 readonly resolve?: Resolve | undefined
58 readonly is?: Is | undefined
59 readonly decode?: Decode | undefined
60 readonly make?: Make | undefined
61 readonly decodeEffect: Parser
62 readonly parser: Parser
63 readonly makeEffect: Parser
64}
65
66class InterpretedEntry implements Entry {
67 readonly ast: SchemaAST.AST
68 declare private cachedDecodeEffect: Parser | undefined
69 declare private cachedMakeEffect: Parser | undefined
70
71 constructor(ast: SchemaAST.AST) {
72 this.ast = ast
73 }
74
75 get decodeEffect(): Parser {
76 return this.cachedDecodeEffect ??= Interpreter.compile(this.ast, decodeChild)
77 }
78
79 get parser(): Parser {
80 return this.decodeEffect
81 }
82
83 get makeEffect(): Parser {
84 return this.cachedMakeEffect ??= Interpreter.compile(this.ast, makeChild, makeField)
85 }
86}
87
88class CompilerEntry extends InterpretedEntry {
89 readonly compiled: Compiled | undefined
90 readonly resolve: Resolve
91
92 constructor(ast: SchemaAST.AST, compiled: Compiled | undefined, resolve: Resolve) {
93 super(ast)
94 this.compiled = compiled
95 this.resolve = resolve
96 }
97
98 private save<K extends keyof Entry>(key: K, value: Entry[K]): Entry[K] {
99 Object.defineProperty(this, key, { value })
100 return value
101 }
102
103 private operation<K extends DecoderOperation>(key: K): CompiledDecoder[K] | undefined {
104 const compiled = this.compiled
105 return typeof compiled === "function" ? compiled(this.ast, this.resolve, key) : compiled?.[key]
106 }
107
108 get is(): Is | undefined {
109 return this.save("is", this.operation("is"))
110 }
111
112 get decode(): Decode | undefined {
113 return this.save("decode", this.operation("decode"))
114 }
115
116 get make(): Make | undefined {
117 return this.save("make", this.operation("make"))
118 }
119
120 override get decodeEffect(): Parser {
121 return this.save(
122 "decodeEffect",
123 this.operation("decodeEffect") ?? Interpreter.compile(this.ast, (ast) => lazyParser(this.resolve, ast, "parser"))
124 )
125 }
126
127 override get parser(): Parser {
128 const decode = this.decode
129 return decode === undefined
130 ? this.decodeEffect
131 : this.save("parser", withDecode(decode, () => this.decodeEffect))
132 }
133
134 override get makeEffect(): Parser {
135 const makeEffect = this.operation("makeEffect")
136 if (makeEffect !== undefined) return this.save("makeEffect", makeEffect)
137 const child = (ast: SchemaAST.AST): Parser => lazyParser(this.resolve, ast, "makeEffect")
138 return this.save(
139 "makeEffect",
140 Interpreter.compile(
141 this.ast,
142 child,
143 (ast) => Interpreter.compileField(ast, child)
144 )
145 )
146 }
147}
148
149/** @internal */
150export function withDecode(fastDecode: Decode, decodeEffect: () => Parser): Parser {
151 let detailed: Parser | undefined
152 return (input, options) => {
153 if (input !== InternalParser.missing) {
154 try {
155 const value = fastDecode(input, options)
156 if (value !== invalid) return value === input ? InternalParser.sameExit : InternalParser.succeed(value)
157 } catch (error) {
158 return Effect.die(error)
159 }
160 }
161 return (detailed ??= decodeEffect())(input, options)
162 }
163}
164
165/** @internal */
166export function lazyParser(
167 resolve: Resolve,
168 ast: SchemaAST.AST,
169 operation: "parser" | "decodeEffect" | "makeEffect"
170): Parser {
171 const entry = resolve(ast)
172 if (entry.compiled === undefined || Object.hasOwn(entry, operation)) {
173 return entry[operation]
174 }
175 let parser: Parser | undefined
176 return (input, options) => (parser ??= entry[operation])(input, options)
177}
178
179/** @internal */
180export function resolve(ast: SchemaAST.AST): Entry {
181 const cached = cache.get(ast)
182 if (cached !== undefined) return cached
183 const entry = compiler === undefined
184 ? new InterpretedEntry(ast)
185 : new CompilerEntry(ast, compiler(ast, resolve), resolve)
186 cache.set(ast, entry)
187 return entry
188}
189
190/** @internal */
191export function set(ast: SchemaAST.AST, decoder: DecoderSource | undefined, resolveChild: Resolve = resolve): Entry {
192 if (decoder !== undefined) activateCompilerAdapters()
193 const entry = new CompilerEntry(ast, decoder, resolveChild)
194 cache.set(ast, entry)
195 return entry
196}
197
198/** @internal */
199export function setCompiler(
200 ast: SchemaAST.AST,
201 compile: Compile,
202 resolveChild: Resolve = resolve
203): Entry {
204 activateCompilerAdapters()
205 const entry = new CompilerEntry(ast, compile, resolveChild)
206 cache.set(ast, entry)
207 return entry
208}
209
210/** @internal */
211export function install(compile: CompileSource): void {
212 activateCompilerAdapters()
213 compiler = compile
214}
215
216/** @internal */
217export function enable(ast: SchemaAST.AST, compile: CompileSource): void {
218 activateCompilerAdapters()
219 const scoped: Resolve = (child) => {
220 const cached = cache.get(child)
221 return cached !== undefined && (cached.compiled !== undefined || cached.resolve === scoped)
222 ? cached
223 : set(child, compile(child, scoped), scoped)
224 }
225 const decoder = compile(ast, scoped)
226 const cached = cache.get(ast)
227 if (decoder !== undefined || cached?.compiled === undefined) {
228 set(ast, decoder, scoped)
229 }
230}