import { iterableFirst , iterableEvery , setMap } from "collection-utils" ; import { Type , ClassType , setOperationCasesEqual , ClassProperty , isPrimitiveStringTypeKind } from "../Type" ; import { removeNullFromType } from "../TypeUtils" ; import { defined , panic } from "../support/Support" ; import { TypeGraph , TypeRef } from "../TypeGraph" ; import { StringTypeMapping } from "../TypeBuilder" ; import { GraphRewriteBuilder } from "../GraphRewriting" ; import { unifyTypes , unionBuilderForUnification } from "../UnifyClasses" ; import { MarkovChain , load , evaluate } from "../MarkovChain" ; const mapSizeThreshold = 20 ; const stringMapSizeThreshold = 50 ; let markovChain = undefined ; function nameProbability ( name ) { if ( markovChain === undefined ) { markovChain = load ( ) ; } return evaluate ( markovChain , name ) ; } function shouldBeMap ( properties ) { const numProperties = properties . size ; if ( numProperties < 2 ) return undefined ; if ( iterableEvery ( properties . keys ( ) , n => /^[0-9]+$/ . test ( n ) ) ) { return setMap ( properties . values ( ) , cp => cp . type ) ; } if ( numProperties < stringMapSizeThreshold && iterableEvery ( properties . values ( ) , cp => isPrimitiveStringTypeKind ( cp . type . kind ) || cp . type . kind === "null" ) ) { return undefined ; } if ( numProperties < mapSizeThreshold ) { const names = Array . from ( properties . keys ( ) ) ; const probabilities = names . map ( nameProbability ) ; const product = probabilities . reduce ( ( a , b ) => a * b , 1 ) ; const probability = Math . pow ( product , 1 / numProperties ) ; const exponent = 5 ; const scale = Math . pow ( 22 , exponent ) ; const limit = Math . pow ( numProperties + 2 , exponent ) / scale + ( 0.0025 - Math . pow ( 3 , exponent ) / scale ) ; if ( probability > limit ) return undefined ; } let firstNonNullCases = undefined ; const allCases = new Set < Type > ( ) ; let canBeMap = true ; for ( const [ , p ] of properties ) { const nn = removeNullFromType ( p . type ) [ 1 ] ; if ( nn . size > 0 ) { if ( firstNonNullCases !== undefined ) { if ( ! setOperationCasesEqual ( nn , firstNonNullCases , true , ( a , b ) => a . structurallyCompatible ( b , true ) ) ) { canBeMap = false ; break ; } } else { firstNonNullCases = nn ; } } allCases . add ( p . type ) ; } if ( ! canBeMap ) { return undefined ; } return allCases ; } export function inferMaps ( graph , stringTypeMapping , conflateNumbers , debugPrintReconstitution ) { function replaceClass ( setOfOneClass , builder , forwardingRef ) { const c = defined ( iterableFirst ( setOfOneClass ) ) ; const properties = c . getProperties ( ) ; const shouldBe = shouldBeMap ( properties ) ; if ( shouldBe === undefined ) { return panic ( `We shouldn't be replacing class ${ c . getCombinedName ( ) } with a map` ) ; } return builder . getMapType ( c . getAttributes ( ) , unifyTypes ( shouldBe , c . getAttributes ( ) , builder , unionBuilderForUnification ( builder , false , false , conflateNumbers ) , conflateNumbers ) , forwardingRef ) ; } const classesToReplace = Array . from ( graph . allNamedTypesSeparated ( ) . objects ) . filter ( o => { if ( ! ( o instanceof ClassType ) ) return false ; return ! o . isFixed && shouldBeMap ( o . getProperties ( ) ) !== undefined ; } ) as ClassType [ ] ; return graph . rewrite ( "infer maps" , stringTypeMapping , false , classesToReplace . map ( c => [ c ] ) , debugPrintReconstitution , replaceClass ) ; }