1/**
2 * Generates static JavaScript modules that install Schema decoders in the shared
3 * registry. Generated modules use the same runtime support as the JIT compiler,
4 * without importing source generation or constructing functions dynamically.
5 *
6 * @stability unstable
7 * @since 4.0.0
8 */
9import * as Codegen from "../internal/schema/codegen.ts"
10import * as SchemaAST from "../SchemaAST.ts"
11import type { runtime } from "./SchemaCompiler/runtime.ts"
12
13const helper = (name: keyof typeof runtime): string => `runtime.${name}`
14const decoderOperationOrder: ReadonlyArray<Codegen.DecoderOperation> = [
15 "is",
16 "decode",
17 "make",
18 "decodeEffect",
19 "makeEffect"
20]
21const operationOrder: ReadonlyArray<Operation> = ["decode", "is", "make"]
22
23const compiler = (sources: ReadonlyMap<Codegen.DecoderOperation, string>): string | undefined => {
24 const cases = decoderOperationOrder.flatMap((key) => {
25 const source = sources.get(key)
26 return source === undefined ? [] : [`case ${JSON.stringify(key)}:{${source}}`]
27 }).join("")
28 return cases.length === 0 ? undefined : `switch(operation){${cases}}`
29}
30
31/**
32 * A parser operation prepared by {@link compile}.
33 *
34 * @stability unstable
35 * @category models
36 * @since 4.0.0
37 */
38export type Operation = "decode" | "is" | "make"
39
40type PlannedOperation = Operation | "decodeEffect"
41
42/**
43 * An exact AST and the parser operations to prepare for it.
44 *
45 * @stability unstable
46 * @category models
47 * @since 4.0.0
48 */
49export interface Target {
50 /** The registry key installed by the generated module. */
51 readonly ast: SchemaAST.AST
52 /** The operations that should be compiled for this AST. */
53 readonly operations: ReadonlyArray<Operation>
54}
55
56/**
57 * Generates a JavaScript ES module exporting `install(asts): void` for an
58 * ordered array of compilation targets.
59 *
60 * **When to use**
61 *
62 * Use to prepare decoders at build time for environments that disallow dynamic
63 * function construction. Save the returned source as a JavaScript module, then
64 * call its `install` export with the target ASTs in the same order before using
65 * parsers. Use a one-element array for a single schema.
66 *
67 * **Details**
68 *
69 * Generation does not install decoders or execute checks and transformations.
70 * Installation uses the same registry as `SchemaCompiler.set`; normal
71 * `SchemaParser` functions consume those entries. Generated validators and
72 * Struct and homogeneous Array loops are static functions. They share diagnostic and
73 * asynchronous continuation helpers with the interpreter. A single synchronous
74 * transformation between supported leaf types can use generated orchestration.
75 * Other detailed traversals, transformations, and middleware use the interpreter
76 * with registry-resolved children. Transformations and middleware are not replayed.
77 * Only the requested operation families and their static dependencies are
78 * emitted. Installation registers roots and dependencies in the same registry;
79 * each generated operation initializes on first use. Missing operations retain
80 * the lazy interpreter fallback. Repeated ASTs and shared dependencies are
81 * installed once by identity. Fast paths can still inline dependency code into
82 * multiple parent decoders. An empty array generates a module whose installation
83 * does nothing.
84 * Construction uses independently lazy `make` and `makeEffect` operations.
85 * Pure fixed Struct and homogeneous Array constructors can use `make` for a
86 * synchronous fast path; failures delegate to `makeEffect` for detailed issues.
87 * Tuple, Record, Union, leaf, and Class constructors use the existing
88 * interpreter. Constructor defaults and Class source schemas are read from the
89 * supplied ASTs, not serialized or executed during generation. `make` is
90 * omitted whenever replay could repeat observable construction work.
91 *
92 * **Gotchas**
93 *
94 * Regenerate the module whenever the schema definition or Effect version
95 * changes. Installation trusts that the runtime array has the same length and
96 * target order, and its ASTs have the same definitions and sharing as at build
97 * time. Functions and symbols are read from those ASTs, not serialized.
98 * Suspend thunks are not evaluated during generation; their contents and other
99 * unsupported nodes use the interpreter.
100 * Installation replaces generated entries, but parsers that already captured
101 * older entries keep them. Type-side and flipped ASTs are separate registry
102 * keys; generate and install them separately when needed. Importing the
103 * generated module alone does not install anything.
104 * In particular, target `SchemaAST.toType(schema.ast)` with `make` to prepare
105 * construction when it differs from the encoded root. Target
106 * `SchemaAST.flip(schema.ast)` with `decode` to prepare encoding. Unsupported
107 * operations use the interpreter while statically installed children remain
108 * available.
109 *
110 * @stability unstable
111 * @category compilation
112 * @since 4.0.0
113 */
114export const compile = (targets: ReadonlyArray<Target>): string => {
115 interface PlannedNode {
116 readonly index: number
117 readonly name: string
118 readonly reference: string
119 readonly requested: Set<PlannedOperation>
120 readonly sources: Map<Codegen.DecoderOperation, string>
121 readonly attempted: Set<Codegen.DecoderOperation>
122 readonly compilable: boolean
123 }
124
125 const seen = new Map<SchemaAST.AST, PlannedNode>()
126 const bindings: Array<string> = []
127 const factories: Array<string> = []
128 const factoryNames = new Map<string, string>()
129 const installations: Array<string> = []
130
131 const addSource = (node: SchemaAST.AST, plan: PlannedNode, operation: Codegen.DecoderOperation): boolean => {
132 if (plan.attempted.has(operation)) return plan.sources.has(operation)
133 plan.attempted.add(operation)
134 if (!plan.compilable) return false
135 const source = Codegen.generate(node, operation)
136 if (source === undefined) return false
137 plan.sources.set(operation, source)
138 return true
139 }
140
141 const visitDependencies = (node: SchemaAST.AST, name: string, operation: PlannedOperation): void => {
142 switch (node._tag) {
143 case "Declaration":
144 node.typeParameters.forEach((child, index) => visit(child, `${name}.typeParameters[${index}]`, operation))
145 break
146 case "TemplateLiteral":
147 node.parts.forEach((child, index) => visit(child, `${name}.parts[${index}]`, operation))
148 break
149 case "Arrays":
150 node.elements.forEach((child, index) => visit(child, `${name}.elements[${index}]`, operation))
151 node.rest.forEach((child, index) => visit(child, `${name}.rest[${index}]`, operation))
152 break
153 case "Objects":
154 node.propertySignatures.forEach((property, index) =>
155 visit(property.type, `${name}.propertySignatures[${index}].type`, operation)
156 )
157 node.indexSignatures.forEach((signature, index) => {
158 visit(
159 SchemaAST.parameterFromPropertyKey(signature.parameter),
160 `${helper("parameterFromPropertyKey")}(${name}.indexSignatures[${index}].parameter)`,
161 operation
162 )
163 visit(signature.type, `${name}.indexSignatures[${index}].type`, operation)
164 })
165 break
166 case "Union":
167 node.types.forEach((child, index) => visit(child, `${name}.types[${index}]`, operation))
168 break
169 }
170 node.encoding?.forEach((link, index) => visit(link.to, `${name}.encoding[${index}].to`, operation))
171 if (operation === "make") {
172 const descriptor = SchemaAST.getConstructorDescriptor(node)
173 if (descriptor !== undefined) {
174 visit(descriptor.link.to, `${helper("getConstructorDescriptor")}(${name}).link.to`, operation)
175 }
176 }
177 }
178
179 function visit(node: SchemaAST.AST, reference: string, operation: PlannedOperation): void {
180 let plan = seen.get(node)
181 if (plan === undefined) {
182 const index = seen.size
183 plan = {
184 index,
185 name: `a${index}`,
186 reference,
187 requested: new Set(),
188 sources: new Map(),
189 attempted: new Set(),
190 compilable: Codegen.shouldCompileParser(node)
191 }
192 seen.set(node, plan)
193 }
194 if (plan.requested.has(operation)) return
195 plan.requested.add(operation)
196
197 switch (operation) {
198 case "decode":
199 addSource(node, plan, "decode")
200 visit(node, reference, "decodeEffect")
201 break
202 case "decodeEffect":
203 addSource(node, plan, "decodeEffect")
204 visitDependencies(node, plan.name, operation)
205 break
206 case "is":
207 if (!addSource(node, plan, "is")) visit(node, reference, "decode")
208 break
209 case "make":
210 addSource(node, plan, "make")
211 addSource(node, plan, "makeEffect")
212 visitDependencies(node, plan.name, operation)
213 break
214 }
215 }
216
217 targets.forEach((target, index) => {
218 for (const operation of operationOrder) {
219 if (target.operations.includes(operation)) visit(target.ast, `asts[${index}]`, operation)
220 }
221 })
222 for (const plan of seen.values()) {
223 bindings.push(`const ${plan.name}=${plan.reference};`)
224 const source = compiler(plan.sources)
225 if (source !== undefined) {
226 let factory = factoryNames.get(source)
227 if (factory === undefined) {
228 factory = `d${factoryNames.size}`
229 factoryNames.set(source, factory)
230 factories.push(`function ${factory}(ast,resolve,operation){const R=runtime;${source}}`)
231 }
232 installations.push(`${helper("setCompiler")}(${plan.name},${factory});`)
233 }
234 }
235 return [
236 "// Generated by SchemaAOTCompiler. Regenerate after schema or Effect changes.",
237 "import { runtime } from \"effect/schema/SchemaCompiler/runtime\";",
238 ...factories,
239 "/** @param {ReadonlyArray<import(\"effect/SchemaAST\").AST>} asts */",
240 "export function install(asts){",
241 ...bindings,
242 ...installations,
243 "}",
244 ""
245 ].join("\n")
246}