Skip to content

Commit d7dd9d3

Browse files
authored
Added versions for approximations (#332)
1 parent da338ff commit d7dd9d3

File tree

7 files changed

+284
-103
lines changed

7 files changed

+284
-103
lines changed

jacodb-approximations/src/main/kotlin/org/jacodb/approximation/Approximations.kt

Lines changed: 193 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import org.jacodb.api.jvm.JcClassExtFeature
2121
import org.jacodb.api.jvm.JcClassOrInterface
2222
import org.jacodb.api.jvm.JcClasspath
2323
import org.jacodb.api.jvm.JcDatabase
24+
import org.jacodb.api.jvm.JcDatabasePersistence
2425
import org.jacodb.api.jvm.JcFeature
2526
import org.jacodb.api.jvm.JcField
2627
import org.jacodb.api.jvm.JcInstExtFeature
@@ -30,18 +31,17 @@ import org.jacodb.api.jvm.RegisteredLocation
3031
import org.jacodb.api.jvm.cfg.JcInstList
3132
import org.jacodb.api.jvm.cfg.JcRawInst
3233
import org.jacodb.api.storage.StorageContext
34+
import org.jacodb.api.storage.ers.Entity
35+
import org.jacodb.api.storage.ers.EntityIterable
3336
import org.jacodb.api.storage.ers.compressed
34-
import org.jacodb.approximation.TransformerIntoVirtual.transformMethodIntoVirtual
3537
import org.jacodb.approximation.annotation.Approximate
38+
import org.jacodb.approximation.annotation.Version
3639
import org.jacodb.impl.cfg.JcInstListImpl
3740
import org.jacodb.impl.fs.className
38-
import org.jacodb.impl.storage.dslContext
3941
import 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
4342
import org.jacodb.impl.storage.txn
4443
import org.jacodb.impl.types.RefKind
44+
import org.objectweb.asm.tree.AnnotationNode
4545
import org.objectweb.asm.tree.ClassNode
4646
import java.util.concurrent.ConcurrentHashMap
4747
import 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+
162283
private 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

203372
private val approximationAnnotationClassName = Approximate::class.qualifiedName!!
373+
private val versionAnnotationClassName = Version::class.qualifiedName!!
204374

205375
@JvmInline
206376
value class ApproximationClassName(val className: String) {

0 commit comments

Comments
 (0)