import { globalValue } from "@fncts/base/data/Global"; import { ASTTag } from "./AST.js"; import { getKeysForIndexSignature, memoize } from "./utils.js"; /** * @tsplus getter fncts.schema.Schema equals */ export function equals(self: Schema): (a: A, b: A) => boolean { const eq = self.eq; return (a, b) => eq.equals(b)(a); } /** * @tsplus getter fncts.schema.Schema eq */ export function eq(self: Schema): Eq { return goMemo(self.ast); } const eqMemoMap = globalValue(Symbol.for("fncts.schema.Eq.eqMemoMap"), () => new WeakMap>()); function goMemo(ast: AST): Eq { const memo = eqMemoMap.get(ast); if (memo) { return memo; } const eq = go(ast); eqMemoMap.set(ast, eq); return eq; } function go(ast: AST): Eq { AST.concrete(ast); switch (ast._tag) { case ASTTag.Declaration: return ast.annotations .get(ASTAnnotation.EqHook) .map((eq) => eq(...ast.typeParameters.map(go))) .getOrElse(Eq.strict); case ASTTag.Literal: case ASTTag.UniqueSymbol: case ASTTag.UndefinedKeyword: case ASTTag.VoidKeyword: case ASTTag.UnknownKeyword: case ASTTag.AnyKeyword: case ASTTag.NumberKeyword: case ASTTag.BooleanKeyword: case ASTTag.BigIntKeyword: case ASTTag.SymbolKeyword: case ASTTag.ObjectKeyword: case ASTTag.TemplateLiteral: case ASTTag.StringKeyword: return Eq.strict; case ASTTag.NeverKeyword: return Eq.never as Eq; case ASTTag.Tuple: { const elements = ast.elements.map((element) => goMemo(element.type)); const rest = ast.rest.map((rest) => rest.map((ast) => goMemo(ast))); return Eq>({ equals: (y) => (x) => { if (x.length !== y.length) { return false; } let i = 0; for (; i < elements.length; i++) { const eq = elements[i]!; const xi = x[i]; const yi = y[i]; if (!eq.equals(yi)(xi)) { return false; } } if (rest.isJust()) { const head = rest.value.unsafeHead!; const tail = rest.value.tail; for (; i < x.length - tail.length; i++) { if (!head.equals(y[i])(x[i])) { return false; } } for (let j = 0; j < tail.length; j++) { i += j; const eq = elements[i]!; const xi = x[i]; const yi = y[i]; if (!eq.equals(yi)(xi)) { return false; } } } return true; }, }); } case ASTTag.TypeLiteral: { const propertySignatureTypes = ast.propertySignatures.map((ps) => go(ps.type)); const indexSignatures = ast.indexSignatures.map((is) => [go(is.parameter), go(is.type)] as const); const requiredEqs: Record> = {}; const optionalEqs: Record> = {}; for (let i = 0; i < propertySignatureTypes.length; i++) { const ps = ast.propertySignatures[i]!; const name = ps.name; if (!ps.isOptional) { requiredEqs[name] = propertySignatureTypes[i]!; } else { optionalEqs[name] = propertySignatureTypes[i]!; } } let output = Eq.struct(requiredEqs, optionalEqs); for (let i = 0; i < indexSignatures.length; i++) { const [, type] = indexSignatures[i]!; output = output.intersection( Eq>({ equals: (y) => (x) => { for (const key of getKeysForIndexSignature(x, ast.indexSignatures[i]!.parameter)) { if (key in requiredEqs || key in optionalEqs) { continue; } if (!type.equals(y[key])(x[key])) { return false; } } return true; }, }), ); } return output; } case ASTTag.Union: return Eq.union(ast.types.map(goMemo).toArray); case ASTTag.Lazy: { const f = () => goMemo(ast.getAST()); const get = memoize>(f); return Eq({ equals: (y) => (x) => get(f).equals(y)(x), }); } case ASTTag.Enum: return Eq.strict; case ASTTag.Transform: return goMemo(ast.to); case ASTTag.Refinement: return goMemo(ast.from); case ASTTag.Validation: return goMemo(ast.from); } }