@@ -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" , approxSymbol.compressed)
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+)$" ).matches(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