1import type * as Cause from "../../Cause.ts"
2import * as Equal from "../../Equal.ts"
3import * as Equivalence from "../../Equivalence.ts"
4import { memoize } from "../../Function.ts"
5import * as Predicate from "../../Predicate.ts"
6import type * as Schema from "../../Schema.ts"
7import * as SchemaAST from "../../SchemaAST.ts"
8import * as SchemaParser from "../../SchemaParser.ts"
9import * as InternalAnnotations from "./annotations.ts"
10
11/** @internal */
12export const toEquivalence = memoize((ast: SchemaAST.AST): Equivalence.Equivalence<any> => {
13 return recur(ast)
14})
15
16function recur(ast: SchemaAST.AST): Equivalence.Equivalence<any> {
17 // ---------------------------------------------
18 // handle annotations
19 // ---------------------------------------------
20 const annotation = InternalAnnotations.resolve(ast)?.["toEquivalence"] as
21 | Schema.Annotations.ToEquivalence.Declaration<any, ReadonlyArray<any>>
22 | undefined
23 if (annotation) {
24 return annotation(SchemaAST.isDeclaration(ast) ? ast.typeParameters.map(recur) : [])
25 }
26 switch (ast._tag) {
27 case "Never":
28 return Equivalence.strictEqual()
29 case "Declaration":
30 return declarationEquivalence(ast)
31 case "Null":
32 case "Undefined":
33 case "Void":
34 case "Unknown":
35 case "Any":
36 case "String":
37 case "Number":
38 case "Boolean":
39 case "BigInt":
40 case "Symbol":
41 case "Literal":
42 case "UniqueSymbol":
43 case "ObjectKeyword":
44 case "Enum":
45 case "TemplateLiteral":
46 return Equal.equals
47 case "Arrays": {
48 const elements = ast.elements.map(recur)
49 const rest = ast.rest.map(recur)
50 const [head, ...tail] = rest
51 const tailLength = tail.length
52 return Equivalence.make((a, b) => {
53 if (!Array.isArray(a) || !Array.isArray(b)) {
54 return false
55 }
56 const len = a.length
57 if (len !== b.length) {
58 return false
59 }
60 // ---------------------------------------------
61 // handle elements
62 // ---------------------------------------------
63 let i = 0
64 for (; i < Math.min(len, ast.elements.length); i++) {
65 if (!elements[i](a[i], b[i])) {
66 return false
67 }
68 }
69 // ---------------------------------------------
70 // handle rest element
71 // ---------------------------------------------
72 if (rest.length > 0) {
73 for (; i < len - tailLength; i++) {
74 if (!head(a[i], b[i])) {
75 return false
76 }
77 }
78 // ---------------------------------------------
79 // handle post rest elements
80 // ---------------------------------------------
81 for (let j = 0; j < tailLength; j++) {
82 if (!tail[j](a[i + j], b[i + j])) {
83 return false
84 }
85 }
86 }
87 return true
88 })
89 }
90 case "Objects": {
91 if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
92 return Equal.equals
93 }
94 const propertySignatures = ast.propertySignatures.map((ps) => recur(ps.type))
95 const indexSignatures = ast.indexSignatures.map((is) => recur(is.type))
96 return Equivalence.make((a, b) => {
97 if (!Predicate.isObject(a) || !Predicate.isObject(b)) {
98 return false
99 }
100 // ---------------------------------------------
101 // handle property signatures
102 // ---------------------------------------------
103 for (let i = 0; i < propertySignatures.length; i++) {
104 const ps = ast.propertySignatures[i]
105 const name = ps.name
106 const aHas = Object.hasOwn(a, name)
107 const bHas = Object.hasOwn(b, name)
108 if (SchemaAST.isOptional(ps.type)) {
109 if (aHas !== bHas) {
110 return false
111 }
112 }
113 if (aHas && bHas && !propertySignatures[i](a[name], b[name])) {
114 return false
115 }
116 }
117 // ---------------------------------------------
118 // handle index signatures
119 // ---------------------------------------------
120 for (let i = 0; i < indexSignatures.length; i++) {
121 const is = ast.indexSignatures[i]
122 const aKeys = SchemaAST.getIndexSignatureKeys(a, is.parameter)
123 const bKeys = SchemaAST.getIndexSignatureKeys(b, is.parameter)
124
125 if (aKeys.length !== bKeys.length) return false
126
127 for (let j = 0; j < aKeys.length; j++) {
128 const key = aKeys[j]
129 if (!Object.hasOwn(b, key) || !indexSignatures[i](a[key], b[key])) {
130 return false
131 }
132 }
133 }
134 return true
135 })
136 }
137 case "Union": {
138 const types = SchemaAST.toType(ast).types
139 const index = SchemaAST.getCandidateIndex(types)
140 const compiled = types.map((candidate, i) => [SchemaParser._is(candidate), recur(ast.types[i])] as const)
141 return Equivalence.make((a, b) => {
142 const candidates = index(a, false)
143 for (let i = 0; i < candidates.length; i++) {
144 const [is, equivalence] = compiled[candidates[i]]
145 if (is(a) && is(b)) {
146 return equivalence(a, b)
147 }
148 }
149 return false
150 })
151 }
152 case "Suspend": {
153 let equivalence: Equivalence.Equivalence<any>
154 return Equivalence.make((a, b) => (equivalence ??= toEquivalence(ast.thunk()))(a, b))
155 }
156 }
157}
158
159function declarationEquivalence(ast: SchemaAST.Declaration): Equivalence.Equivalence<any> {
160 const representation = (ast.annotations as Schema.Annotations.Declaration<any> | undefined)?.representation
161 if (representation === undefined) return Equal.equals
162 switch (representation.id) {
163 case "effect/schema/Option": {
164 const [value] = declarationTypeParameters(ast)
165 return (a, b) => a._tag === b._tag && (a._tag === "None" || value(a.value, b.value))
166 }
167 case "effect/schema/Result": {
168 const [success, failure] = declarationTypeParameters(ast)
169 return (a, b) =>
170 a._tag === b._tag &&
171 (a._tag === "Success" ? success(a.success, b.success) : failure(a.failure, b.failure))
172 }
173 case "effect/schema/CauseReason": {
174 const [error, defect] = declarationTypeParameters(ast)
175 return causeReasonEquivalence(error, defect)
176 }
177 case "effect/schema/Cause": {
178 const [error, defect] = declarationTypeParameters(ast)
179 return causeEquivalence(error, defect)
180 }
181 case "effect/schema/Exit": {
182 const [value, error, defect] = declarationTypeParameters(ast)
183 const cause = causeEquivalence(error, defect)
184 return (a, b) => a._tag === b._tag && (a._tag === "Success" ? value(a.value, b.value) : cause(a.cause, b.cause))
185 }
186 case "effect/schema/ReadonlyMap": {
187 const [key, value] = declarationTypeParameters(ast)
188 return Equal.makeCompareMap(key, value)
189 }
190 case "effect/schema/ReadonlySet":
191 return Equal.makeCompareSet(declarationTypeParameters(ast)[0])
192 case "effect/schema/RegExp":
193 return (a: globalThis.RegExp, b: globalThis.RegExp) => a.source === b.source && a.flags === b.flags
194 case "effect/schema/URL":
195 return (a: globalThis.URL, b: globalThis.URL) => a.toString() === b.toString()
196 default:
197 return Equal.equals
198 }
199}
200
201function declarationTypeParameters(ast: SchemaAST.Declaration) {
202 return ast.typeParameters.map(recur)
203}
204
205function causeReasonEquivalence<E>(
206 error: Equivalence.Equivalence<E>,
207 defect: Equivalence.Equivalence<unknown>
208): Equivalence.Equivalence<Cause.Reason<E>> {
209 return (a, b) => {
210 if (a._tag !== b._tag) return false
211 switch (a._tag) {
212 case "Fail":
213 return error(a.error, (b as Cause.Fail<E>).error)
214 case "Die":
215 return defect(a.defect, (b as Cause.Die).defect)
216 case "Interrupt":
217 return a.fiberId === (b as Cause.Interrupt).fiberId
218 }
219 }
220}
221
222function causeEquivalence<E>(
223 error: Equivalence.Equivalence<E>,
224 defect: Equivalence.Equivalence<unknown>
225): Equivalence.Equivalence<Cause.Cause<E>> {
226 const reasons = Equivalence.Array(causeReasonEquivalence(error, defect))
227 return (a, b) => reasons(a.reasons, b.reasons)
228}