@@ -21,6 +21,7 @@ import org.jacodb.api.jvm.JcClassExtFeature
2121import  org.jacodb.api.jvm.JcClassOrInterface 
2222import  org.jacodb.api.jvm.JcClasspath 
2323import  org.jacodb.api.jvm.JcDatabase 
24+ import  org.jacodb.api.jvm.JcDatabasePersistence 
2425import  org.jacodb.api.jvm.JcFeature 
2526import  org.jacodb.api.jvm.JcField 
2627import  org.jacodb.api.jvm.JcInstExtFeature 
@@ -30,18 +31,17 @@ import org.jacodb.api.jvm.RegisteredLocation
3031import  org.jacodb.api.jvm.cfg.JcInstList 
3132import  org.jacodb.api.jvm.cfg.JcRawInst 
3233import  org.jacodb.api.storage.StorageContext 
34+ import  org.jacodb.api.storage.ers.Entity 
35+ import  org.jacodb.api.storage.ers.EntityIterable 
3336import  org.jacodb.api.storage.ers.compressed 
34- import  org.jacodb.approximation.TransformerIntoVirtual.transformMethodIntoVirtual 
3537import  org.jacodb.approximation.annotation.Approximate 
38+ import  org.jacodb.approximation.annotation.Version 
3639import  org.jacodb.impl.cfg.JcInstListImpl 
3740import  org.jacodb.impl.fs.className 
38- import  org.jacodb.impl.storage.dslContext 
3941import  org.jacodb.impl.storage.execute 
40- import  org.jacodb.impl.storage.jooq.tables.references.ANNOTATIONS 
41- import  org.jacodb.impl.storage.jooq.tables.references.ANNOTATIONVALUES 
42- import  org.jacodb.impl.storage.jooq.tables.references.CLASSES 
4342import  org.jacodb.impl.storage.txn 
4443import  org.jacodb.impl.types.RefKind 
44+ import  org.objectweb.asm.tree.AnnotationNode 
4545import  org.objectweb.asm.tree.ClassNode 
4646import  java.util.concurrent.ConcurrentHashMap 
4747import  java.util.concurrent.ConcurrentMap 
@@ -59,11 +59,18 @@ import java.util.concurrent.ConcurrentMap
5959 *  otherwise you might get incomplete mapping. 
6060 *  See [JcDatabase.awaitBackgroundJobs]. 
6161 */  
62- object  Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeature {
62+ class  Approximations (
63+     versions :  List <VersionInfo >
64+ ) : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeature {
65+ 
66+     private  val  instSubstitutorForApproximations =  InstSubstitutorForApproximations (this )
67+     private  val  transformerIntoVirtual =  TransformerIntoVirtual (this )
6368
6469    private  val  originalToApproximation:  ConcurrentMap <OriginalClassName , ApproximationClassName > =  ConcurrentHashMap ()
6570    private  val  approximationToOriginal:  ConcurrentMap <ApproximationClassName , OriginalClassName > =  ConcurrentHashMap ()
6671
72+     private  val  versionMap:  VersionMap  =  versions.associate { it.target to it.version }
73+ 
6774    override  suspend  fun  query (classpath :  JcClasspath , req :  Any? ): Sequence <Any ?> {
6875        //  returns an empty sequence for now, all requests are made using
6976        //  findApproximationOrNull and findOriginalByApproximation functions
@@ -73,7 +80,29 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
7380    override  fun  newIndexer (
7481        jcdb :  JcDatabase ,
7582        location :  RegisteredLocation 
76-     ): ByteCodeIndexer  =  ApproximationIndexer (originalToApproximation, approximationToOriginal)
83+     ): ByteCodeIndexer  =  ApproximationIndexer (originalToApproximation, approximationToOriginal, versionMap)
84+ 
85+     private  val  Entity .annotationNameId:  Long? 
86+         get() =  getCompressed<Long >(" nameId" 
87+ 
88+     private  val  Entity .annotationValueNameId:  Long? 
89+         get() =  getCompressedBlob<Long >(" nameId" 
90+ 
91+     private  fun  EntityIterable.annotationPrimitiveValueByName (valueNameId :  Long ): Long?  {
92+         val  value =  find { valueNameId ==  it.getCompressedBlob<Long >(" nameId" 
93+         return  value?.getCompressedBlob<Long >(" primitiveValue" 
94+     }
95+ 
96+     private  fun  EntityIterable.annotationStringValueByName (
97+         persistence :  JcDatabasePersistence ,
98+         valueNameId :  Long 
99+     ): String?  {
100+         val  valueId =  annotationPrimitiveValueByName(valueNameId)
101+         if  (valueId !=  null )
102+             return  persistence.findSymbolName(valueId)
103+ 
104+         return  null 
105+     }
77106
78107    override  fun  onSignal (signal :  JcSignal ) {
79108        if  (signal is  JcSignal .BeforeIndexing ) {
@@ -82,24 +111,53 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
82111            persistence.read { context -> 
83112                context.execute(
84113                    sqlAction =  {
85-                         context.dslContext.select(CLASSES .NAME , ANNOTATIONVALUES .CLASS_SYMBOL )
86-                             .from(ANNOTATIONS )
87-                             .join(CLASSES ).on(ANNOTATIONS .CLASS_ID .eq(CLASSES .ID ))
88-                             .join(ANNOTATIONVALUES ).on(ANNOTATIONVALUES .ANNOTATION_ID .eq(ANNOTATIONS .ID ))
89-                             .where(
90-                                 ANNOTATIONS .ANNOTATION_NAME .eq(approxSymbol).and (
91-                                     ANNOTATIONVALUES .NAME .eq(" value" 
92-                                 )
93-                             )
94-                             .fetch().asSequence().map { record ->  record.value1() to record.value2() }
114+                         TODO (" support versions for SQL persistence" 
115+ //                         context.dslContext.select(CLASSES.NAME, ANNOTATIONVALUES.CLASS_SYMBOL)
116+ //                             .from(ANNOTATIONS)
117+ //                             .join(CLASSES).on(ANNOTATIONS.CLASS_ID.eq(CLASSES.ID))
118+ //                             .join(ANNOTATIONVALUES).on(ANNOTATIONVALUES.ANNOTATION_ID.eq(ANNOTATIONS.ID))
119+ //                             .where(
120+ //                                 ANNOTATIONS.ANNOTATION_NAME.eq(approxSymbol).and(
121+ //                                     ANNOTATIONVALUES.NAME.eq("value")
122+ //                                 )
123+ //                             )
124+ //                             .fetch().asSequence().map { record -> record.value1() to record.value2() }
95125                    },
96126                    noSqlAction =  {
97127                        val  valueId =  persistence.findSymbolId(" value" 
128+                         val  versionsId =  persistence.findSymbolId(" versions" 
129+                         val  versionSymbol =  persistence.findSymbolId(versionAnnotationClassName)
130+                         val  targetId =  persistence.findSymbolId(" target" 
131+                         val  fromVersionId =  persistence.findSymbolId(" fromVersion" 
132+                         val  toVersionId =  persistence.findSymbolId(" toVersion" 
98133                        context.txn.find(" Annotation" " nameId" 
99134                            .filter { it.getCompressedBlob<Int >(" refKind" ==  RefKind .CLASS .ordinal }
100-                             .flatMap { annotation -> 
135+                             .mapNotNull { annotation -> 
136+                                 val  values =  annotation.getLinks(" values" 
137+                                 val  versionsValue =  values.filterTo(mutableListOf ()) {
138+                                     versionsId ==  it.annotationValueNameId
139+                                 }
140+                                 if  (versionsValue.isEmpty())
141+                                     return @mapNotNull annotation  to values
142+ 
143+                                 val  versionMatches =  versionsValue.any { versionValue -> 
144+                                     val  versionAnnotation =  versionValue.getLink(" refAnnotation" 
145+                                     check(versionSymbol ==  versionAnnotation.annotationNameId)
146+                                     val  versionValues =  versionAnnotation.getLinks(" values" 
147+                                     val  target =  versionValues.annotationStringValueByName(persistence, targetId)
148+                                         ? :  error(" unable to find `target` value in `Version` annotation" 
149+                                     val  fromVersion =  versionValues.annotationStringValueByName(persistence, fromVersionId)
150+                                         ? :  error(" unable to find `fromVersion` value in `Version` annotation" 
151+                                     val  toVersion =  versionValues.annotationStringValueByName(persistence, toVersionId)
152+                                         ? :  error(" unable to find `toVersion` value in `Version` annotation" 
153+                                     VersionsIntervalInfo (target, fromVersion, toVersion).matches(versionMap)
154+                                 }
155+                                 if  (versionMatches)
156+                                     annotation  to values
157+                                 else  null 
158+                             }.flatMap { (annotation, values) -> 
101159                                annotation.getLink(" ref" let  { clazz -> 
102-                                     annotation.getLinks( " values" ) .map { clazz to it }
160+                                     values.map { clazz to it }
103161                                }
104162                            }.filter { (_, annotationValue) -> 
105163                                valueId ==  annotationValue[" nameId" 
@@ -125,13 +183,13 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
125183        val  approximationName =  findApproximationByOriginOrNull(clazz.name.toOriginalName()) ? :  return  null 
126184        val  approximationClass =  clazz.classpath.findClassOrNull(approximationName) ? :  return  null 
127185
128-         return  approximationClass.declaredFields.map { TransformerIntoVirtual .transformIntoVirtualField(clazz, it) }
186+         return  approximationClass.declaredFields.map { transformerIntoVirtual .transformIntoVirtualField(clazz, it) }
129187    }
130188
131189    /* *
132190     * Returns a list of [JcEnrichedVirtualMethod] if there is an approximation for [clazz] and null otherwise. 
133191     */  
134-     override  fun  methodsOf (clazz :  JcClassOrInterface ): List <JcMethod >?  {
192+     override  fun  methodsOf (clazz :  JcClassOrInterface ): List <JcMethod >?  =   with (transformerIntoVirtual)  {
135193        val  approximationName =  findApproximationByOriginOrNull(clazz.name.toOriginalName()) ? :  return  null 
136194        val  approximationClass =  clazz.classpath.findClassOrNull(approximationName) ? :  return  null 
137195
@@ -141,7 +199,7 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
141199    }
142200
143201    override  fun  transformRawInstList (method :  JcMethod , list :  JcInstList <JcRawInst >): JcInstList <JcRawInst > {
144-         return  JcInstListImpl (list.map { it.accept(InstSubstitutorForApproximations ) })
202+         return  JcInstListImpl (list.map { it.accept(instSubstitutorForApproximations ) })
145203    }
146204
147205    /* *
@@ -159,10 +217,118 @@ object Approximations : JcFeature<Any?, Any?>, JcClassExtFeature, JcInstExtFeatu
159217    ): String?  =  approximationToOriginal[className]?.className
160218}
161219
220+ data class  VersionInfo (
221+     val  target :  String ,
222+     val  version :  String ,
223+ ) {
224+     init  {
225+         check(version.isVersion)
226+     }
227+ }
228+ 
229+ private  typealias  VersionMap  =  Map <String , String >
230+ 
231+ private  data class  VersionsIntervalInfo (
232+     val  target :  String ,
233+     val  fromVersion :  String ,
234+     val  toVersion :  String ,
235+ ) {
236+     init  {
237+         check(fromVersion.isVersion)
238+         check(toVersion.isVersion)
239+     }
240+ 
241+     fun  matches (versionMap :  VersionMap ): Boolean  {
242+         check(fromVersion.isVersion &&  toVersion.isVersion)
243+ 
244+         val  version =  versionMap[this .target] ? :  return  false 
245+ 
246+         val  fromVersionNumbers =  fromVersion.toNumbers
247+         val  toVersionNumbers =  toVersion.toNumbers
248+         val  versionNumbers =  version.toNumbers
249+ 
250+         return  versionNumbers.isVersionInRange(fromVersionNumbers, toVersionNumbers)
251+     }
252+ }
253+ 
254+ /* *
255+  * Checks if the string is a version in the form of 3 numbers separated by dots, e.g., "1.2.3". 
256+  */  
257+ private  val  String .isVersion:  Boolean  get() = 
258+     Regex (" ^(\\ d+)\\ .(\\ d+)\\ .(\\ d+)$" this )
259+ 
260+ private  val  String .toNumbers:  IntArray  get() {
261+     val  numbers =  this .split(' .' 
262+     check(numbers.size ==  3 )
263+     return  IntArray (numbers.size) { i ->  numbers[i].toInt() }
264+ }
265+ 
266+ /* *
267+  * Checks if a version (array of 3 numbers) is within the inclusive range defined by fromVersion and toVersion (also arrays of 3 numbers). 
268+  * Example: 1.1.1 <= 1.2.0 <= 2.0.0 
269+  */  
270+ private  fun  IntArray.isVersionInRange (fromVersion :  IntArray , toVersion :  IntArray ): Boolean  {
271+     require(this .size ==  3  &&  fromVersion.size ==  3  &&  toVersion.size ==  3 ) { " All version arrays must have size 3" 
272+     for  (i in  0 .. 2 ) {
273+         if  (this [i] <  fromVersion[i]) return  false 
274+         if  (this [i] >  fromVersion[i]) break 
275+     }
276+     for  (i in  0 .. 2 ) {
277+         if  (this [i] >  toVersion[i]) return  false 
278+         if  (this [i] <  toVersion[i]) break 
279+     }
280+     return  true 
281+ }
282+ 
162283private  class  ApproximationIndexer (
163284    private  val  originalToApproximation :  ConcurrentMap <OriginalClassName , ApproximationClassName >,
164-     private  val  approximationToOriginal :  ConcurrentMap <ApproximationClassName , OriginalClassName >
285+     private  val  approximationToOriginal :  ConcurrentMap <ApproximationClassName , OriginalClassName >,
286+     private  val  versionMap :  VersionMap 
165287) : ByteCodeIndexer {
288+ 
289+     private  fun  annotationValueByName (versionValues :  List <Any >, name :  String ): Any?  {
290+         val  valueNameIdx =  versionValues.indexOf(name)
291+         if  (valueNameIdx ==  - 1 )
292+             return  null 
293+ 
294+         val  valueIdx =  valueNameIdx +  1 
295+         if  (valueIdx >=  versionValues.size)
296+             return  null 
297+ 
298+         return  versionValues[valueIdx]
299+     }
300+ 
301+     private  fun  annotationStringValueByName (versionValues :  List <Any >, name :  String ): String?  {
302+         return  annotationValueByName(versionValues, name) as ?  String 
303+     }
304+ 
305+     private  val  AnnotationNode .versionIntervalInfo:  VersionsIntervalInfo  get() {
306+         val  target =  annotationStringValueByName(values, " target" 
307+             ? :  error(" unable to find `target` value in `Version` annotation" 
308+         val  from =  annotationStringValueByName(values, " fromVersion" 
309+             ? :  error(" unable to find `target` value in `Version` annotation" 
310+         val  to =  annotationStringValueByName(values, " toVersion" 
311+             ? :  error(" unable to find `target` value in `Version` annotation" 
312+         return  VersionsIntervalInfo (target, from, to)
313+     }
314+ 
315+     private  fun  checkVersion (approximationAnnotation :  AnnotationNode ): Boolean  {
316+         val  versions =  annotationValueByName(approximationAnnotation.values, " versions" as ?  List <* >
317+             //  When `Approximate` annotation does not contain `Version` annotation, it matches any version
318+             ? :  return  true 
319+ 
320+         if  (versions.isEmpty())
321+             return  true 
322+ 
323+         for  (version in  versions) {
324+             version as  AnnotationNode 
325+             if  (version.versionIntervalInfo.matches(versionMap))
326+                 return  true 
327+         }
328+ 
329+         return  false 
330+     }
331+ 
166332    override  fun  index (classNode :  ClassNode ) {
167333        val  annotations =  classNode.visibleAnnotations ? :  return 
168334
@@ -171,6 +337,9 @@ private class ApproximationIndexer(
171337            approximationAnnotationClassName in  it.desc.className
172338        } ? :  return 
173339
340+         if  (! checkVersion(approximationAnnotation))
341+             return 
342+ 
174343        //  Extract a name of the target class for this approximation
175344        val  target =  approximationAnnotation.values.filterIsInstance< org.objectweb.asm.Type > ().single()
176345
@@ -201,6 +370,7 @@ private class ApproximationIndexer(
201370}
202371
203372private  val  approximationAnnotationClassName =  Approximate ::class .qualifiedName!! 
373+ private  val  versionAnnotationClassName =  Version ::class .qualifiedName!! 
204374
205375@JvmInline
206376value class  ApproximationClassName (val  className :  String ) {
0 commit comments