1import type { CompiledDecoder } from "../../schema/SchemaCompiler.ts"
2import type { runtime } from "../../schema/SchemaCompiler/runtime.ts"
3import * as SchemaAST from "../../SchemaAST.ts"
4
5const getEncodingChecks = (ast: SchemaAST.AST): SchemaAST.Checks | undefined =>
6 "encodingChecks" in ast ? ast.encodingChecks : undefined
7const getExpectedKeys = (ast: SchemaAST.Objects): ReadonlyArray<PropertyKey> =>
8 ast.propertySignatures.map((p) => typeof p.name === "number" ? String(p.name) : p.name)
9
10const isOptional = (ast: SchemaAST.AST): boolean => ast.context?.isOptional ?? false
11
12const maxGeneratedDepth = 256
13/** @internal */
14export const maxGeneratedNodes = 2048
15
16type Emission = "unsupported" | "decode" | "is"
17type Operation = "decode" | "is"
18
19const failureExpression = (operation: Operation): string => operation === "decode" ? "I" : "false"
20
21type Facts = {
22 supported: boolean
23 outputFree: boolean
24 makeSafe: boolean
25 hasOneOf: boolean
26 nodes: number
27 height: number
28}
29
30const factsCache = new WeakMap<SchemaAST.AST, Facts>()
31
32const unsupportedFacts: Facts = {
33 supported: false,
34 outputFree: false,
35 makeSafe: false,
36 hasOneOf: false,
37 nodes: 1,
38 height: 0
39}
40
41const isMakeSafeNode = (ast: SchemaAST.AST): boolean =>
42 ast._tag !== "Union" && ast._tag !== "Declaration" && ast._tag !== "Suspend" &&
43 (ast._tag !== "Objects" || ast.indexSignatures.length === 0) &&
44 ast.encoding === undefined &&
45 ast.context?.constructorDefault === undefined
46
47const addChild = (
48 facts: Facts,
49 child: SchemaAST.AST,
50 depth: number,
51 budget: { remaining: number }
52): boolean => {
53 if (child.encoding !== undefined || child._tag === "Declaration" || child._tag === "Suspend") {
54 facts.supported = false
55 return false
56 }
57 switch (child._tag) {
58 case "TemplateLiteral":
59 case "Arrays":
60 case "Objects":
61 case "Union": {
62 const childFacts = getFacts(child, depth, budget)
63 if (childFacts === undefined) return false
64 if (!childFacts.supported) {
65 facts.supported = false
66 return false
67 }
68 if (!childFacts.outputFree) facts.outputFree = false
69 if (!childFacts.makeSafe) facts.makeSafe = false
70 if (childFacts.hasOneOf) facts.hasOneOf = true
71 facts.nodes += childFacts.nodes
72 if (childFacts.height >= facts.height) facts.height = childFacts.height + 1
73 return true
74 }
75 default:
76 if (--budget.remaining < 0 || depth > maxGeneratedDepth) return false
77 if (!isMakeSafeNode(child)) facts.makeSafe = false
78 facts.nodes++
79 if (facts.height === 0) facts.height = 1
80 return true
81 }
82}
83
84const stop = (ast: SchemaAST.AST, facts: Facts): Facts | undefined => {
85 if (facts.supported) return undefined
86 factsCache.set(ast, unsupportedFacts)
87 return unsupportedFacts
88}
89
90const getFacts = (
91 ast: SchemaAST.AST,
92 depth = 0,
93 budget = { remaining: maxGeneratedNodes }
94): Facts | undefined => {
95 const cached = factsCache.get(ast)
96 if (cached !== undefined) {
97 budget.remaining -= cached.nodes
98 return budget.remaining < 0 || depth + cached.height > maxGeneratedDepth ? undefined : cached
99 }
100 if (--budget.remaining < 0 || depth > maxGeneratedDepth) return undefined
101 const facts: Facts = {
102 supported: true,
103 outputFree: true,
104 makeSafe: true,
105 hasOneOf: ast._tag === "Union" && ast.options?.mode === "oneOf",
106 nodes: 1,
107 height: 0
108 }
109 const next = depth + 1
110 switch (ast._tag) {
111 case "TemplateLiteral":
112 for (const part of ast.parts) {
113 if (!addChild(facts, part, next, budget)) return stop(ast, facts)
114 }
115 // matchPart only segments the input; the tuple parser enforces oneOf.
116 if (facts.hasOneOf) {
117 facts.supported = false
118 return stop(ast, facts)
119 }
120 break
121 case "Arrays":
122 for (const element of ast.elements) {
123 if (!addChild(facts, element, next, budget)) return stop(ast, facts)
124 }
125 for (const element of ast.rest) {
126 if (!addChild(facts, element, next, budget)) return stop(ast, facts)
127 }
128 break
129 case "Objects":
130 for (const property of ast.propertySignatures) {
131 if (!addChild(facts, property.type, next, budget)) return stop(ast, facts)
132 }
133 for (const signature of ast.indexSignatures) {
134 if (
135 !addChild(facts, SchemaAST.parameterFromPropertyKey(signature.parameter), next, budget) ||
136 !addChild(facts, signature.type, next, budget)
137 ) return stop(ast, facts)
138 }
139 break
140 case "Union":
141 for (const type of ast.types) {
142 if (!addChild(facts, type, next, budget)) return stop(ast, facts)
143 }
144 break
145 }
146 facts.outputFree = ast._tag === "Arrays" || ast._tag === "Objects" || ast._tag === "Union"
147 ? ast.checks === undefined && facts.outputFree
148 : true
149 facts.makeSafe = facts.makeSafe && isMakeSafeNode(ast)
150 factsCache.set(ast, facts)
151 return facts
152}
153
154/** @internal */
155const getEmission = (ast: SchemaAST.AST, depth = 0, local = false): Emission => {
156 if (!local && ast.encoding !== undefined || ast._tag === "Declaration" || ast._tag === "Suspend") {
157 return "unsupported"
158 }
159 const facts = getFacts(ast, depth)
160 if (facts === undefined || !facts.supported) return "unsupported"
161 return facts.outputFree ? "is" : "decode"
162}
163
164const isMakeSafe = (ast: SchemaAST.AST): boolean => getFacts(ast)?.makeSafe ?? false
165
166const shouldCompileMake = (ast: SchemaAST.AST): ast is SchemaAST.Arrays | SchemaAST.Objects =>
167 (ast._tag === "Arrays" && ast.elements.length === 0 && ast.rest.length === 1 ||
168 ast._tag === "Objects" && ast.propertySignatures.length > 0 && ast.indexSignatures.length === 0) &&
169 isMakeSafe(ast)
170
171type Emitter = {
172 readonly statements: Array<string>
173 readonly helpers: Array<string>
174 readonly initializers: Array<string>
175 readonly decoderHelpers: Map<SchemaAST.AST, string>
176 readonly unionHelpers: Map<SchemaAST.Union, string>
177 readonly bindings: Array<Binding>
178 readonly constantIndexes: Map<unknown, number>
179 next: number
180}
181
182const variable = (emitter: Emitter): string => `v${emitter.next++}`
183
184const propertyKey = (emitter: Emitter, key: PropertyKey, reference: string): string =>
185 typeof key === "string" ? JSON.stringify(key) : constant(emitter, key, reference)
186
187const propertyPresence = (input: string, key: string, name: PropertyKey): string =>
188 name === "__proto__" ? `Object.hasOwn(${input},${key})` : `${key} in ${input}`
189
190const assignProperty = (output: string, key: string, value: string, name: PropertyKey): string =>
191 name === "__proto__"
192 ? `Object.defineProperty(${output},${key},{value:${value},writable:true,enumerable:true,configurable:true})`
193 : `${output}[${key}]=${value}`
194
195const constant = (emitter: Emitter, value: unknown, reference: string): string => {
196 const cached = emitter.constantIndexes.get(value)
197 if (cached !== undefined) return `C[${cached}]`
198 const index = emitter.bindings.length
199 emitter.bindings.push({ value, reference })
200 emitter.constantIndexes.set(value, index)
201 return `C[${index}]`
202}
203
204const needsPresenceCheck = (ast: SchemaAST.AST): boolean => {
205 switch (ast._tag) {
206 case "Undefined":
207 case "Void":
208 case "Any":
209 case "Unknown":
210 return true
211 case "Union":
212 return ast.types.some(needsPresenceCheck)
213 default:
214 return false
215 }
216}
217
218const propertyNeedsPresenceCheck = (name: PropertyKey, ast: SchemaAST.AST): boolean =>
219 name === "__proto__" || needsPresenceCheck(ast)
220
221/** @internal */
222export const shouldCompileParser = (ast: SchemaAST.AST, local = false): boolean => {
223 if (!local && ast.encoding !== undefined) return true
224 if (SchemaAST.getConstructorDescriptor(ast) !== undefined) return true
225 if (ast.checks !== undefined || getEncodingChecks(ast) !== undefined) return true
226 switch (ast._tag) {
227 case "TemplateLiteral":
228 case "Arrays":
229 case "Objects":
230 case "Union":
231 return true
232 default:
233 return false
234 }
235}
236
237const lookupMemberValues = (ast: SchemaAST.AST): ReadonlyArray<unknown> | undefined => {
238 if (ast.checks !== undefined || getEncodingChecks(ast) !== undefined || ast.encoding !== undefined) return undefined
239 switch (ast._tag) {
240 case "Null":
241 return [null]
242 case "Undefined":
243 return [undefined]
244 case "Literal":
245 return [ast.literal]
246 case "UniqueSymbol":
247 return [ast.symbol]
248 case "Enum":
249 return [...new Set(ast.enums.map((entry) => entry[1]))]
250 default:
251 return undefined
252 }
253}
254
255const lookupMemberReferences = (ast: SchemaAST.AST, path: string): ReadonlyArray<string> => {
256 switch (ast._tag) {
257 case "Null":
258 return ["null"]
259 case "Undefined":
260 return ["void 0"]
261 case "Literal":
262 return [`${path}.literal`]
263 case "UniqueSymbol":
264 return [`${path}.symbol`]
265 case "Enum": {
266 const references = new Map<unknown, string>()
267 ast.enums.forEach((entry, index) => {
268 if (!references.has(entry[1])) references.set(entry[1], `${path}.enums[${index}][1]`)
269 })
270 return [...references.values()]
271 }
272 default:
273 throw new Error(`Unsupported lookup member: ${ast._tag}`)
274 }
275}
276
277function emit(
278 ast: SchemaAST.AST,
279 input: string,
280 statements: Array<string>,
281 emitter: Emitter,
282 operation: Operation,
283 path: string
284): string {
285 const output = emitBase(ast, input, statements, emitter, operation, path)
286 const invalid = failureExpression(operation)
287 const encodingChecks = getEncodingChecks(ast)
288 const astConstant = ast.checks !== undefined || encodingChecks !== undefined
289 ? constant(emitter, ast, path)
290 : undefined
291 if (encodingChecks !== undefined) {
292 statements.push(`if(K(${astConstant},${input},1,o))return ${invalid}`)
293 }
294 if (ast.checks === undefined) return operation === "decode" ? output : "true"
295 const checked = variable(emitter)
296 statements.push(
297 `const ${checked}=${output}`,
298 `if(K(${astConstant},${checked},0,o))return ${invalid}`
299 )
300 return operation === "decode" ? checked : "true"
301}
302
303const emitDecoderHelper = (ast: SchemaAST.AST, emitter: Emitter, operation: Operation, path: string): string => {
304 const cached = emitter.decoderHelpers.get(ast)
305 if (cached !== undefined) return cached
306 const name = `d${emitter.next++}`
307 emitter.decoderHelpers.set(ast, name)
308 const statements: Array<string> = []
309 const output = emit(ast, "i", statements, emitter, operation, path)
310 emitter.helpers.push(
311 `function ${name}(i,o){${statements.join(";")};return ${output}}`
312 )
313 return name
314}
315
316const emitUnionHelper = (ast: SchemaAST.Union, emitter: Emitter, operation: Operation, path: string): string => {
317 const cached = emitter.unionHelpers.get(ast)
318 if (cached !== undefined) return cached
319 const name = `u${emitter.next++}`
320 emitter.unionHelpers.set(ast, name)
321 const decoders = ast.types.map((type, index) =>
322 emitDecoderHelper(type, emitter, operation, `${path}.types[${index}]`)
323 )
324 emitter.initializers.push(`const ${name}=[${decoders.join(",")}]`)
325 return name
326}
327
328const emitIndexes = (
329 ast: SchemaAST.Objects,
330 input: string,
331 output: string | undefined,
332 statements: Array<string>,
333 emitter: Emitter,
334 operation: Operation,
335 path: string,
336 indexKeys: string
337): void => {
338 const fixedKeys = output === undefined || ast.propertySignatures.length === 0
339 ? undefined
340 : constant(
341 emitter,
342 new Set(getExpectedKeys(ast)),
343 `new Set(${runtimeReference("getExpectedKeys")}(${path}))`
344 )
345 for (let signatureIndex = 0; signatureIndex < ast.indexSignatures.length; signatureIndex++) {
346 const signature = ast.indexSignatures[signatureIndex]
347 const signaturePath = `${path}.indexSignatures[${signatureIndex}]`
348 const keys = variable(emitter)
349 const index = variable(emitter)
350 const key = variable(emitter)
351 const parameter = signature.parameter
352 statements.push(
353 `const ${keys}=${indexKeys}?.[${signatureIndex}]??${
354 parameter._tag === "String" && parameter.checks === undefined
355 ? `Object.keys(${input})`
356 : `G(${input},${constant(emitter, parameter, `${signaturePath}.parameter`)},o)`
357 }`
358 )
359 const loop: Array<string> = [`const ${key}=${keys}[${index}]`]
360 const decodedKey = parameter._tag === "String" && parameter.checks === undefined && parameter.encoding === undefined
361 ? key
362 : emit(
363 SchemaAST.parameterFromPropertyKey(parameter),
364 key,
365 loop,
366 emitter,
367 operation,
368 `${runtimeReference("parameterFromPropertyKey")}(${signaturePath}.parameter)`
369 )
370 const value = variable(emitter)
371 loop.push(`const ${value}=${input}[${key}]`)
372 const decoded = emit(signature.type, value, loop, emitter, operation, `${signaturePath}.type`)
373 if (output !== undefined) {
374 const assign =
375 `if(${decodedKey}==="__proto__")Object.defineProperty(${output},${decodedKey},{value:${decoded},writable:true,enumerable:true,configurable:true});else ${output}[${decodedKey}]=${decoded}`
376 loop.push(
377 fixedKeys === undefined
378 ? assign
379 : `if(!${fixedKeys}.has(${key})&&!${fixedKeys}.has(${decodedKey})){${assign}}`
380 )
381 }
382 statements.push(`for(let ${index}=0;${index}<${keys}.length;${index}++){${loop.join(";")}}`)
383 }
384}
385
386const emitBase = (
387 ast: SchemaAST.AST,
388 input: string,
389 statements: Array<string>,
390 emitter: Emitter,
391 operation: Operation,
392 path: string
393): string => {
394 const needsValue = operation === "decode"
395 const invalid = failureExpression(operation)
396 switch (ast._tag) {
397 case "Null":
398 statements.push(`if(${input}!==null)return ${invalid}`)
399 return input
400 case "Undefined":
401 statements.push(`if(${input}!==void 0)return ${invalid}`)
402 return input
403 case "Void":
404 return "void 0"
405 case "Never":
406 statements.push(`return ${invalid}`)
407 return input
408 case "Any":
409 case "Unknown":
410 return input
411 case "ObjectKeyword":
412 statements.push(
413 `if((${input}===null||typeof ${input}!=="object")&&typeof ${input}!=="function")return ${invalid}`
414 )
415 return input
416 case "Enum": {
417 const values = constant(
418 emitter,
419 new Set(ast.enums.map((entry) => entry[1])),
420 `new Set(${path}.enums.map(entry=>entry[1]))`
421 )
422 statements.push(`if(!${values}.has(${input}))return ${invalid}`)
423 return input
424 }
425 case "UniqueSymbol": {
426 const value = constant(emitter, ast.symbol, `${path}.symbol`)
427 statements.push(`if(${input}!==${value})return ${invalid}`)
428 return input
429 }
430 case "Literal": {
431 const value = constant(emitter, ast.literal, `${path}.literal`)
432 statements.push(`if(${input}!==${value})return ${invalid}`)
433 return input
434 }
435 case "String":
436 statements.push(`if(typeof ${input}!=="string")return ${invalid}`)
437 return input
438 case "Number":
439 statements.push(`if(typeof ${input}!=="number")return ${invalid}`)
440 return input
441 case "Boolean":
442 statements.push(`if(typeof ${input}!=="boolean")return ${invalid}`)
443 return input
444 case "Symbol":
445 statements.push(`if(typeof ${input}!=="symbol")return ${invalid}`)
446 return input
447 case "BigInt":
448 statements.push(`if(typeof ${input}!=="bigint")return ${invalid}`)
449 return input
450 case "TemplateLiteral": {
451 const template = constant(emitter, ast, path)
452 statements.push(`if(!T(${template},${input},o))return ${invalid}`)
453 return input
454 }
455 case "Arrays": {
456 statements.push(`if(!Array.isArray(${input}))return ${invalid}`)
457 const length = variable(emitter)
458 statements.push(`const ${length}=${input}.length`)
459 const elementLength = ast.elements.length
460 const requiredElementLength = ast.elements.findIndex(isOptional)
461 const minimumElementLength = requiredElementLength === -1 ? elementLength : requiredElementLength
462 const tailLength = Math.max(0, ast.rest.length - 1)
463 if (ast.rest.length === 0) {
464 statements.push(
465 minimumElementLength === elementLength
466 ? `if(${length}!==${elementLength})return ${invalid}`
467 : `if(${length}<${minimumElementLength}||${length}>${elementLength})return ${invalid}`
468 )
469 if (minimumElementLength === elementLength) {
470 const elements = ast.elements.map((element, index) => {
471 const value = variable(emitter)
472 statements.push(`const ${value}=${input}[${index}]`)
473 return emit(element, value, statements, emitter, operation, `${path}.elements[${index}]`)
474 })
475 return needsValue ? `[${elements.join(",")}]` : input
476 }
477 const output = needsValue ? variable(emitter) : undefined
478 if (output !== undefined) statements.push(`const ${output}=new Array(${length})`)
479 for (let index = 0; index < elementLength; index++) {
480 const value = variable(emitter)
481 const elementStatements: Array<string> = [`const ${value}=${input}[${index}]`]
482 const decoded = emit(
483 ast.elements[index],
484 value,
485 elementStatements,
486 emitter,
487 operation,
488 `${path}.elements[${index}]`
489 )
490 if (output !== undefined) elementStatements.push(`${output}[${index}]=${decoded}`)
491 statements.push(
492 index < minimumElementLength
493 ? elementStatements.join(";")
494 : `if(${index}<${length}){${elementStatements.join(";")}}`
495 )
496 }
497 return output ?? input
498 }
499 statements.push(`if(${length}<${minimumElementLength + tailLength})return ${invalid}`)
500 const output = needsValue ? variable(emitter) : undefined
501 if (output !== undefined) statements.push(`const ${output}=new Array(${length})`)
502 for (let index = 0; index < elementLength; index++) {
503 const value = variable(emitter)
504 const elementStatements: Array<string> = [`const ${value}=${input}[${index}]`]
505 const decoded = emit(
506 ast.elements[index],
507 value,
508 elementStatements,
509 emitter,
510 operation,
511 `${path}.elements[${index}]`
512 )
513 if (output !== undefined) elementStatements.push(`${output}[${index}]=${decoded}`)
514 statements.push(
515 index < minimumElementLength
516 ? elementStatements.join(";")
517 : `if(${index}<${length}){${elementStatements.join(";")}}`
518 )
519 }
520 const index = variable(emitter)
521 const restStatements: Array<string> = []
522 const value = variable(emitter)
523 restStatements.push(`const ${value}=${input}[${index}]`)
524 const decoded = emit(ast.rest[0], value, restStatements, emitter, operation, `${path}.rest[0]`)
525 if (output !== undefined) restStatements.push(`${output}[${index}]=${decoded}`)
526 statements.push(
527 `for(let ${index}=${elementLength};${index}<${length}-${tailLength};${index}++){${restStatements.join(";")}}`
528 )
529 for (let index = 0; index < tailLength; index++) {
530 const inputIndex = `${length}-${tailLength - index}`
531 const value = variable(emitter)
532 statements.push(`const ${value}=${input}[${inputIndex}]`)
533 const decoded = emit(ast.rest[index + 1], value, statements, emitter, operation, `${path}.rest[${index + 1}]`)
534 if (output !== undefined) statements.push(`${output}[${inputIndex}]=${decoded}`)
535 }
536 return output ?? input
537 }
538 case "Objects": {
539 if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
540 statements.push(`if(${input}===null||${input}===void 0)return ${invalid}`)
541 return input
542 }
543 statements.push(
544 `if(typeof ${input}!=="object"||${input}===null||Array.isArray(${input}))return ${invalid}`
545 )
546 // Strict parsing collects index keys before reading any declared fields,
547 // just like the interpreter. Reuse that snapshot when decoding the indexes.
548 const object = constant(emitter, ast, path)
549 const strict = `o!==D&&o.onExcessProperty==="error"`
550 const indexKeys = ast.indexSignatures.length > 0 ? variable(emitter) : undefined
551 if (indexKeys !== undefined) {
552 statements.push(
553 `const ${indexKeys}=${strict}?${object}.indexSignatures.map(p=>G(${input},p.parameter,o)):void 0`
554 )
555 }
556 statements.push(
557 `if(${indexKeys ?? strict}&&E(${object},${input},${indexKeys ?? "void 0"}))return ${invalid}`
558 )
559 const output = needsValue ? variable(emitter) : undefined
560 const hasOptional = ast.propertySignatures.some((property) => isOptional(property.type))
561 if (output !== undefined && ast.propertySignatures.length > 0 && !hasOptional) {
562 const properties = ast.propertySignatures.map((property, index) => {
563 const propertyPath = `${path}.propertySignatures[${index}]`
564 const key = propertyKey(emitter, property.name, `${propertyPath}.name`)
565 const outputKey = typeof property.name === "string" && property.name !== "__proto__" ? key : `[${key}]`
566 const value = variable(emitter)
567 if (propertyNeedsPresenceCheck(property.name, property.type)) {
568 statements.push(`if(!(${propertyPresence(input, key, property.name)}))return ${invalid}`)
569 }
570 statements.push(`const ${value}=${input}[${key}]`)
571 return `${outputKey}:${emit(property.type, value, statements, emitter, operation, `${propertyPath}.type`)}`
572 })
573 statements.push(`const ${output}={${properties.join(",")}}`)
574 } else {
575 if (output !== undefined) statements.push(`const ${output}={}`)
576 for (let propertyIndex = 0; propertyIndex < ast.propertySignatures.length; propertyIndex++) {
577 const property = ast.propertySignatures[propertyIndex]
578 const propertyPath = `${path}.propertySignatures[${propertyIndex}]`
579 const key = propertyKey(emitter, property.name, `${propertyPath}.name`)
580 const value = variable(emitter)
581 const propertyStatements: Array<string> = [`const ${value}=${input}[${key}]`]
582 const decoded = emit(property.type, value, propertyStatements, emitter, operation, `${propertyPath}.type`)
583 if (output !== undefined) propertyStatements.push(assignProperty(output, key, decoded, property.name))
584 statements.push(
585 isOptional(property.type)
586 ? `if(${propertyPresence(input, key, property.name)}){${propertyStatements.join(";")}}`
587 : `${
588 propertyNeedsPresenceCheck(property.name, property.type)
589 ? `if(!(${propertyPresence(input, key, property.name)}))return ${invalid};`
590 : ""
591 }${propertyStatements.join(";")}`
592 )
593 }
594 }
595 if (indexKeys !== undefined) emitIndexes(ast, input, output, statements, emitter, operation, path, indexKeys)
596 return output ?? input
597 }
598 case "Union": {
599 const memberValues = ast.types.map(lookupMemberValues)
600 if (memberValues.every((values) => values !== undefined)) {
601 const references = ast.types.map((type, index) => lookupMemberReferences(type, `${path}.types[${index}]`))
602 if (ast.options?.mode !== "oneOf") {
603 const values = constant(emitter, new Set(memberValues.flat()), `new Set([${references.flat().join(",")}])`)
604 statements.push(`if(!${values}.has(${input}))return ${invalid}`)
605 } else {
606 const counts = new Map<unknown, number>()
607 const valueReferences = new Map<unknown, string>()
608 for (let memberIndex = 0; memberIndex < memberValues.length; memberIndex++) {
609 const values = memberValues[memberIndex]
610 for (let valueIndex = 0; valueIndex < values.length; valueIndex++) {
611 const value = values[valueIndex]
612 counts.set(value, (counts.get(value) ?? 0) + 1)
613 if (!valueReferences.has(value)) valueReferences.set(value, references[memberIndex][valueIndex])
614 }
615 }
616 const entries = [...counts].map(([value, count]) => `[${valueReferences.get(value)},${count}]`)
617 const lookup = constant(emitter, counts, `new Map([${entries.join(",")}])`)
618 statements.push(`if(${lookup}.get(${input})!==1)return ${invalid}`)
619 }
620 return input
621 }
622 const candidates = variable(emitter)
623 const output = variable(emitter)
624 const candidate = variable(emitter)
625 const index = variable(emitter)
626 const decoder = variable(emitter)
627 const types = constant(emitter, ast.types, `${path}.types`)
628 const decoders = emitUnionHelper(ast, emitter, operation, path)
629 const select = variable(emitter)
630 emitter.initializers.push(`const ${select}=U(${types})`)
631 statements.push(
632 `const ${candidates}=${select}(${input},false)`,
633 `let ${output}=${invalid},${candidate},${decoder}`
634 )
635 if (ast.options?.mode !== "oneOf") {
636 statements.push(
637 `for(let ${index}=0;${index}<${candidates}.length;${index}++){${decoder}=${decoders}[${candidates}[${index}]];${candidate}=${decoder}(${input},o);if(${candidate}!==${invalid}){${output}=${candidate};break}}`
638 )
639 statements.push(`if(${output}===${invalid})return ${invalid}`)
640 } else {
641 const successes = variable(emitter)
642 statements.push(`let ${successes}=0`)
643 statements.push(
644 `for(let ${index}=0;${index}<${candidates}.length;${index}++){${decoder}=${decoders}[${candidates}[${index}]];${candidate}=${decoder}(${input},o);if(${candidate}!==${invalid}){if(++${successes}>1)return ${invalid};${output}=${candidate}}}`
645 )
646 statements.push(`if(${successes}!==1)return ${invalid}`)
647 }
648 return output
649 }
650 default:
651 throw new Error(`Unsupported Schema AST: ${ast._tag}`)
652 }
653}
654
655/** @internal */
656export interface Binding {
657 readonly value: unknown
658 readonly reference: string
659}
660
661/** @internal */
662export const runtimeReference = (name: keyof typeof runtime): string => `R.${name}`
663
664const runtimeBindings = (aliases: Readonly<Record<string, keyof typeof runtime>>): string =>
665 `const {${Object.entries(aliases).map(([alias, name]) => `${name}:${alias}`).join(",")}}=R;`
666
667/** @internal */
668export interface GeneratedOperation {
669 readonly source: string
670 readonly bindings: ReadonlyArray<Binding>
671}
672
673const emitOperation = (ast: SchemaAST.AST, operation: Operation, path = "ast"): GeneratedOperation => {
674 const emitter: Emitter = {
675 statements: [],
676 helpers: [],
677 initializers: [],
678 decoderHelpers: new Map(),
679 unionHelpers: new Map(),
680 bindings: [],
681 constantIndexes: new Map(),
682 next: 0
683 }
684 const output = emit(ast, "i", emitter.statements, emitter, operation, path)
685 const bindings = {
686 K: "getCheckIssues",
687 T: "matchesTemplateLiteral",
688 U: "getCandidateIndex",
689 G: "getIndexSignatureKeys",
690 D: "defaultParseOptions",
691 E: "hasExcessProperties"
692 } as const
693 const source = `"use strict";${runtimeBindings(operation === "decode" ? { I: "invalid", ...bindings } : bindings)}${
694 emitter.helpers.join(";")
695 };${emitter.initializers.join(";")};return function(i,o){${emitter.statements.join(";")};return ${output}}`
696 return { source, bindings: emitter.bindings }
697}
698
699/** @internal */
700export type DecoderOperation = keyof CompiledDecoder
701
702const renderOperation = (emitted: GeneratedOperation): string =>
703 `(function(C,R){${emitted.source}})([${emitted.bindings.map((binding) => binding.reference).join(",")}],R)`
704
705/** @internal */
706export function generate(ast: SchemaAST.AST, operation: DecoderOperation): string | undefined {
707 if (operation === "make") {
708 if (!shouldCompileMake(ast)) return undefined
709 if (ast._tag === "Objects") {
710 return `if(ast.propertySignatures.some(p=>resolve(p.type).compiled!==void 0))return;return ${
711 renderOperation(emitOperation(ast, "decode"))
712 }`
713 }
714 const element = renderOperation(emitOperation(ast.rest[0], "decode", "ast.rest[0]"))
715 return `const e=resolve(ast.rest[0]),m=e.compiled===void 0?${element}:e.make;if(m===void 0)return;` +
716 "return function(i,o){if(i===R.missing)return R.missing;if(!Array.isArray(i))return R.invalid;" +
717 "const out=new Array(i.length);for(let x=0;x<i.length;x++){const v=m(i[x],o);" +
718 "if(v===R.invalid||v===R.missing)return R.invalid;out[x]=v}" +
719 "return R.getCheckIssues(ast,out,false,o)?R.invalid:out}"
720 }
721 if (operation === "is" || operation === "decode") {
722 if (!shouldCompileParser(ast)) return undefined
723 const emission = getEmission(ast)
724 if (emission === "unsupported" || operation === "is" && emission !== "is") return undefined
725 return "return " + renderOperation(emitOperation(ast, operation))
726 }
727 if (operation === "decodeEffect") {
728 const encoding = emitEncoding(ast)
729 if (encoding !== undefined) return encoding
730 }
731 const object = ast._tag === "Objects" && ast.propertySignatures.length > 0 &&
732 ast.indexSignatures.length === 0 && ast.propertySignatures.length <= maxGeneratedNodes
733 ? emitObject(ast)
734 : "undefined"
735 const array = ast._tag === "Arrays" && ast.elements.length === 0 && ast.rest.length === 1
736 ? emitArray()
737 : "undefined"
738 if (operation === "makeEffect") return `return R.make(ast,resolve,${object},${array})`
739 const checkpoint = ast.encoding !== undefined && getEmission(ast, 0, true) !== "unsupported"
740 ? `()=>${renderOperation(emitOperation(ast, "decode"))}`
741 : "undefined"
742 return `return R.decode(ast,resolve,${object},${getEmission(ast) !== "unsupported"},${checkpoint},${array})`
743}
744
745const emitArray = (): string =>
746 `function({getElement,step,resume}){return function(s,input,index=0,end=input.length){
747 const parser=getElement();let r,t,value;
748 for(;index<end;index++){
749 const item=input[index];r=parser(item,s.options);
750 if(r===R.sameExit&&item!==R.missing){s.output[index]=item;continue}
751 if(!R.effectIsExit(r))return resume(s,item,index,r,end);
752 if(r._tag==="Success"&&(value=r===R.sameExit?item:r[R.args])!==R.missing){s.output[index]=value}
753 else{t=step(s,item,r,index);if(t)return t}
754 }
755 }}`
756
757const inlineTypePredicate = (ast: SchemaAST.AST, input: string, path: string): string | undefined => {
758 switch (ast._tag) {
759 case "Null":
760 return `${input}===null`
761 case "Undefined":
762 return `${input}===void 0`
763 case "Never":
764 return "false"
765 case "Any":
766 case "Unknown":
767 return "true"
768 case "ObjectKeyword":
769 return `((${input}!==null&&typeof ${input}==="object")||typeof ${input}==="function")`
770 case "UniqueSymbol":
771 return `${input}===${path}.symbol`
772 case "Literal":
773 return `${input}===${path}.literal`
774 case "String":
775 return `typeof ${input}==="string"`
776 case "Number":
777 return `typeof ${input}==="number"`
778 case "Boolean":
779 return `typeof ${input}==="boolean"`
780 case "Symbol":
781 return `typeof ${input}==="symbol"`
782 case "BigInt":
783 return `typeof ${input}==="bigint"`
784 case "TemplateLiteral":
785 // Its parser reports structured issues, not an InvalidType failure.
786 return undefined
787 default:
788 return undefined
789 }
790}
791
792const emitEncoding = (ast: SchemaAST.AST): string | undefined => {
793 const links = ast.encoding
794 if (links?.length !== 1 || getEncodingChecks(ast) !== undefined) return undefined
795 const link = links[0]
796 const transformation = link.transformation
797 if (
798 link.to.encoding !== undefined ||
799 getEncodingChecks(link.to) !== undefined ||
800 transformation._tag !== "Transformation" ||
801 transformation.decode._tag !== "Transform"
802 ) {
803 return undefined
804 }
805 const source = inlineTypePredicate(link.to, "i", "ast.encoding[0].to")
806 const target = inlineTypePredicate(ast, "value", "ast")
807 if (source === undefined || target === undefined) return undefined
808 const success = ast.checks === undefined ? "R.succeed(value)" : "R.check(ast,value,o)"
809 const sourceChecks = link.to.checks === undefined
810 ? ""
811 : "const issues=R.getCheckIssues(from,i,false,o);" +
812 "if(issues!==void 0)return R.invalidEncodingChecks(ast,i,issues,o);"
813 return `const transform=ast.encoding[0].transformation.decode.transform,from=ast.encoding[0].to;return function(i,o){try{
814 if(i===R.missing)return R.missingExit;
815 if(!(${source}))return R.invalidEncoding(ast,1,i,i,o);
816 ${sourceChecks}const value=transform(i);
817 if(value===R.missing)return R.missingExit;
818 if(!(${target}))return R.invalidEncoding(ast,0,i,value,o);
819 return ${success}
820 }catch(e){return R.die(e)}}`
821}
822
823const canInlineEncoding = (ast: SchemaAST.AST): ast is SchemaAST.AST & { readonly encoding: SchemaAST.Encoding } =>
824 ast.encoding !== undefined &&
825 ast.checks === undefined &&
826 getEncodingChecks(ast) === undefined &&
827 inlineTypePredicate(ast, "v", "ast") !== undefined &&
828 ast.encoding.every((link, index, links) =>
829 link.to.encoding === undefined &&
830 getEncodingChecks(link.to) === undefined &&
831 // Only the initial source may have checks; they run before any transformation.
832 (link.to.checks === undefined || index === links.length - 1) &&
833 inlineTypePredicate(link.to, "v", "ast") !== undefined &&
834 link.transformation._tag === "Transformation" &&
835 (link.transformation.decode._tag === "Passthrough" || link.transformation.decode._tag === "Transform")
836 )
837
838const inlinePropertyHandler =
839 `const handle=(state,index,property,present,input,result)=>{let value;if(result===R.sameExit){if(!present)return;value=input}else{if(!R.effectIsExit(result))return resume(state,index,result);if(result._tag!=="Success"||(value=result[R.args])===R.missing)return step(state,property,result)}const name=property.name;if(name==="__proto__")Object.defineProperty(state.out,name,{value,writable:true,enumerable:true,configurable:true});else state.out[name]=value}`
840
841const emitObject = (ast: SchemaAST.Objects): string => {
842 const initializers: Array<string> = []
843 const transforms = new Map<object, string>()
844 const bindTransform = (transform: object, path: string): string => {
845 let name = transforms.get(transform)
846 if (name === undefined) {
847 name = `t${transforms.size}`
848 transforms.set(transform, name)
849 initializers.push(`const ${name}=${path}`)
850 }
851 return name
852 }
853 let usesInlinePropertyHandler = false
854 let usesInlinePropertyFailure = false
855 let usesInlineSourceFailure = false
856 const lazyProperties = ast.propertySignatures.every((property) =>
857 typeof property.name !== "symbol" && canInlineEncoding(property.type)
858 )
859 if (lazyProperties) initializers.push("const p=i=>getProperties()[i]")
860 const statements = [
861 "if(i===R.missing)return R.missingExit",
862 "if(o.errors===\"all\"||o.onExcessProperty!==void 0||(o.concurrency!==void 0&&o.concurrency!==1))return fallback(i,o)",
863 "if(typeof i!==\"object\"||i===null||Array.isArray(i))return R.invalidType(ast,i,o)",
864 lazyProperties ? "const out={}" : "const properties=getProperties(),out={}",
865 "const state={ast,input:i,out,options:o,issues:void 0}",
866 "let r,t,value"
867 ]
868 ast.propertySignatures.forEach((property, index) => {
869 const key = typeof property.name === "symbol" ? `properties[${index}].name` : JSON.stringify(String(property.name))
870 const present = property.name === "__proto__" ? `Object.hasOwn(i,${key})` : `${key} in i`
871 const handle = `if(r===R.sameExit){if(h${index}){${assignProperty("out", key, `v${index}`, property.name)}}}else{` +
872 `if(!R.effectIsExit(r))return resume(state,${index},r);` +
873 `if(r._tag==="Success"&&(value=r[R.args])!==R.missing){${assignProperty("out", key, "value", property.name)}}` +
874 `else{t=step(state,p${index},r);if(t)return t}}`
875 const propertyPath = `ast.propertySignatures[${index}].type`
876 if (canInlineEncoding(property.type)) {
877 usesInlinePropertyHandler = true
878 const descriptor = lazyProperties ? `p(${index})` : `p${index}`
879 const handleInline = `t=handle(state,${index},${descriptor},h${index},v${index},r);if(t)return t`
880 const links = property.type.encoding
881 const sourceAst = links[links.length - 1].to
882 const sourcePath = `${propertyPath}.encoding[${links.length - 1}].to`
883 const source = inlineTypePredicate(sourceAst, `v${index}`, sourcePath)!
884 const sourceChecks = sourceAst.checks !== undefined
885 if (sourceChecks) initializers.push(`const s${index}=${sourcePath}`)
886 const checkSource = sourceChecks ? `const c${index}=R.getCheckIssues(s${index},v${index},false,o);` : ""
887 // Share cold diagnostics without adding a call to the successful path.
888 if (lazyProperties && links.length === 1 && property.name !== "__proto__") {
889 usesInlinePropertyFailure = true
890 const transformation = links[0].transformation
891 let decoded = `v${index}`
892 if (transformation._tag === "Transformation" && transformation.decode._tag === "Transform") {
893 const transform = bindTransform(
894 transformation.decode.transform,
895 `${propertyPath}.encoding[0].transformation.decode.transform`
896 )
897 decoded = `${transform}(v${index})`
898 }
899 const predicate = inlineTypePredicate(property.type, `x${index}`, propertyPath)!
900 let run = `const x${index}=${decoded};` +
901 `if(x${index}!==R.missing&&(${predicate})){out[${key}]=x${index}}` +
902 `else{t=invalid(state,${index},h${index},v${index},x${index},o);if(t)return t}`
903 if (sourceChecks) {
904 usesInlineSourceFailure = true
905 run = `${checkSource}if(c${index}===void 0){${run}}` +
906 `else{t=invalidSource(state,${index},h${index},v${index},c${index},o);if(t)return t}`
907 }
908 statements.push(
909 `const h${index}=${present},v${index}=h${index}?i[${key}]:R.missing`,
910 `if(v${index}!==R.missing&&(${source})){${run}}` +
911 `else{t=fallbackProperty(state,${index},h${index},v${index},o);if(t)return t}`
912 )
913 return
914 }
915 const fast: Array<string> = [`let x${index}=v${index};r=void 0;l${index}:{`]
916 if (sourceChecks) {
917 fast.push(
918 `${checkSource}if(c${index}!==void 0){r=R.invalidEncodingChecks(${descriptor}.type,v${index},c${index},o);break l${index}}`
919 )
920 }
921 for (let linkIndex = links.length - 1; linkIndex >= 0; linkIndex--) {
922 const transformation = links[linkIndex].transformation
923 if (transformation._tag === "Transformation" && transformation.decode._tag === "Transform") {
924 const transform = bindTransform(
925 transformation.decode.transform,
926 `${propertyPath}.encoding[${linkIndex}].transformation.decode.transform`
927 )
928 fast.push(`x${index}=${transform}(x${index})`)
929 }
930 const targetPath = linkIndex === 0
931 ? propertyPath
932 : `${propertyPath}.encoding[${linkIndex - 1}].to`
933 const target = linkIndex === 0 ? property.type : links[linkIndex - 1].to
934 const predicate = inlineTypePredicate(target, `x${index}`, targetPath)!
935 fast.push(
936 `if(x${index}===R.missing){r=R.missingExit;break l${index}}else if(!(${predicate})){r=R.invalidEncoding(${descriptor}.type,${linkIndex},v${index},x${index},o);break l${index}}`
937 )
938 }
939 fast.push(
940 `};if(r!==void 0){${handleInline}}else{${assignProperty("out", key, `x${index}`, property.name)}}`
941 )
942 const run = `if(v${index}!==R.missing&&(${source})){${
943 fast.join(";")
944 }}else{r=${descriptor}.parser(v${index},o);${handleInline}}`
945 statements.push(
946 lazyProperties
947 ? `const h${index}=${present},v${index}=h${index}?i[${key}]:R.missing`
948 : `const p${index}=properties[${index}],h${index}=${present},v${index}=h${index}?i[${key}]:R.missing`,
949 run
950 )
951 } else {
952 statements.push(
953 `const p${index}=properties[${index}],h${index}=${present},v${index}=h${index}?i[${key}]:R.missing`,
954 `r=p${index}.parser(v${index},o)`,
955 handle
956 )
957 }
958 })
959 statements.push("return R.succeed(out)")
960 if (usesInlinePropertyHandler) {
961 initializers.unshift(inlinePropertyHandler)
962 }
963 if (usesInlinePropertyFailure) {
964 initializers.push(
965 "const invalid=(state,index,present,input,output,o)=>{const property=p(index);return handle(state,index,property,present,input,output===R.missing?R.missingExit:R.invalidEncoding(property.type,0,input,output,o))}",
966 "const fallbackProperty=(state,index,present,input,o)=>{const property=p(index);return handle(state,index,property,present,input,property.parser(input,o))}"
967 )
968 }
969 if (usesInlineSourceFailure) {
970 initializers.push(
971 "const invalidSource=(state,index,present,input,issues,o)=>{const property=p(index);return handle(state,index,property,present,input,R.invalidEncodingChecks(property.type,input,issues,o))}"
972 )
973 }
974 return `function({ast,getProperties,fallback,resume,step}){${initializers.join(";")};return function(i,o){try{${
975 statements.join(";")
976 }}catch(e){return R.die(e)}}}`
977}