Skip to content

Replaced access path bases with access paths in backward dataflow #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 3, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -127,7 +127,7 @@ jobs:
DEST_DIR="arkanalyzer"
MAX_RETRIES=10
RETRY_DELAY=3 # Delay between retries in seconds
BRANCH="neo/2025-06-16"
BRANCH="neo/2025-06-24"
for ((i=1; i<=MAX_RETRIES; i++)); do
git clone --depth=1 --branch $BRANCH $REPO_URL $DEST_DIR && break
4 changes: 4 additions & 0 deletions usvm-ts-dataflow/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ dependencies {
implementation(Libs.kotlinx_collections)
implementation(Libs.kotlinx_serialization_json)
implementation(Libs.clikt)
runtimeOnly(Libs.logback)

testImplementation(Libs.mockk)
testImplementation(Libs.junit_jupiter_params)
@@ -129,5 +130,8 @@ tasks.shadowJar {
// Provider com.github.ajalt.mordant.terminal.terminalinterface.jna.TerminalInterfaceProviderJna not found
// ```
exclude(dependency("com.github.ajalt.mordant:.*:.*"))

// Keep the logback in shadow jar:
exclude(dependency("ch.qos.logback:logback-classic:.*"))
}
}
Original file line number Diff line number Diff line change
@@ -2,21 +2,17 @@ package org.usvm.dataflow.ts.infer

import org.jacodb.ets.model.EtsMethod
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.model.EtsType
import org.jacodb.impl.cfg.graphs.GraphDominators
import org.usvm.dataflow.ifds.Analyzer
import org.usvm.dataflow.ifds.Edge
import org.usvm.dataflow.ifds.Vertex
import org.usvm.dataflow.ts.graph.EtsApplicationGraph

class BackwardAnalyzer(
val graph: EtsApplicationGraph,
savedTypes: MutableMap<EtsType, MutableList<EtsTypeFact>>,
dominators: (EtsMethod) -> GraphDominators<EtsStmt>,
doAddKnownTypes: Boolean = true,
) : Analyzer<BackwardTypeDomainFact, AnalyzerEvent, EtsMethod, EtsStmt> {

override val flowFunctions = BackwardFlowFunctions(graph, dominators, savedTypes, doAddKnownTypes)
override val flowFunctions = BackwardFlowFunctions(doAddKnownTypes)

override fun handleCrossUnitCall(
caller: Vertex<BackwardTypeDomainFact, EtsStmt>,
@@ -27,7 +23,7 @@ class BackwardAnalyzer(

override fun handleNewEdge(edge: Edge<BackwardTypeDomainFact, EtsStmt>): List<AnalyzerEvent> {
val (startVertex, currentVertex) = edge
val (current, currentFact) = currentVertex
val (current, _) = currentVertex

val method = graph.methodOf(current)
val currentIsExit = current in graph.exitPoints(method)

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -307,6 +307,11 @@ sealed interface EtsTypeFact {
"with" to FunctionEtsTypeFact,
)

internal val numberOrString: EtsTypeFact = mkUnionType(
NumberEtsTypeFact,
StringEtsTypeFact,
)

fun mkUnionType(vararg types: EtsTypeFact): EtsTypeFact = mkUnionType(types.toHashSet())

fun mkUnionType(types: Set<EtsTypeFact>): EtsTypeFact {
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ package org.usvm.dataflow.ts.infer
import org.jacodb.ets.model.EtsMethod
import org.jacodb.ets.model.EtsNopStmt
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.model.EtsType
import org.usvm.dataflow.ifds.Analyzer
import org.usvm.dataflow.ifds.Edge
import org.usvm.dataflow.ifds.Vertex
@@ -12,7 +11,6 @@ import org.usvm.dataflow.ts.graph.EtsApplicationGraph
class ForwardAnalyzer(
val graph: EtsApplicationGraph,
methodInitialTypes: Map<EtsMethod, Map<AccessPathBase, EtsTypeFact>>,
typeInfo: Map<EtsType, EtsTypeFact>,
doAddKnownTypes: Boolean = true,
doAliasAnalysis: Boolean = true,
val doLiveVariablesAnalysis: Boolean = true,
@@ -21,7 +19,6 @@ class ForwardAnalyzer(
override val flowFunctions = ForwardFlowFunctions(
graph = graph,
methodInitialTypes = methodInitialTypes,
typeInfo = typeInfo,
doAddKnownTypes = doAddKnownTypes,
doAliasAnalysis = doAliasAnalysis,
doLiveVariablesAnalysis = doLiveVariablesAnalysis,
Original file line number Diff line number Diff line change
@@ -25,7 +25,6 @@ import org.jacodb.ets.model.EtsReturnStmt
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.model.EtsStringConstant
import org.jacodb.ets.model.EtsThis
import org.jacodb.ets.model.EtsType
import org.jacodb.ets.model.EtsUnclearRefType
import org.jacodb.ets.model.EtsUndefinedConstant
import org.jacodb.ets.model.EtsUnknownType
@@ -46,7 +45,6 @@ private val logger = KotlinLogging.logger {}
class ForwardFlowFunctions(
val graph: EtsApplicationGraph,
val methodInitialTypes: Map<EtsMethod, Map<AccessPathBase, EtsTypeFact>>,
val typeInfo: Map<EtsType, EtsTypeFact>,
val doAddKnownTypes: Boolean = true,
val doAliasAnalysis: Boolean = true,
val doLiveVariablesAnalysis: Boolean = true,
@@ -208,20 +206,10 @@ class ForwardFlowFunctions(

when (val rhv = current.rhv) {
is EtsNewExpr -> {
// val newType = rhv.type
// if (newType is EtsClassType) {
// val cls = graph.cp.classes
// .firstOrNull { it.name == newType.typeName }
// if (cls != null) {
// for (f in cls.fields) {
// val path = lhv + FieldAccessor(f.name)
// result += TypedVariable(path, EtsTypeFact.from(f.type))
// }
// }
// }

val type = typeInfo[rhv.type]
?: EtsTypeFact.ObjectEtsTypeFact(cls = rhv.type, properties = emptyMap())
val type = EtsTypeFact.ObjectEtsTypeFact(
cls = rhv.type,
properties = emptyMap(),
)
addTypeFactWithAliases(lhv, type)
}

Original file line number Diff line number Diff line change
@@ -3,9 +3,8 @@ package org.usvm.dataflow.ts.infer
sealed interface BackwardTypeDomainFact {
data object Zero : BackwardTypeDomainFact

// Requirement
data class TypedVariable(
val variable: AccessPathBase,
val variable: AccessPath,
val type: EtsTypeFact,
) : BackwardTypeDomainFact
}
Original file line number Diff line number Diff line change
@@ -100,7 +100,7 @@
val resolved = when {
suitableTypes.isEmpty() -> error("Should be processed earlier")
suitableTypes.size == 1 -> suitableTypes.single()
suitableTypes.size in 2..5 -> EtsTypeFact.mkUnionType(suitableTypes).simplify()
suitableTypes.size in 2..3 -> EtsTypeFact.mkUnionType(suitableTypes).simplify()
else -> this
}
return resolve(this, resolved)
Original file line number Diff line number Diff line change
@@ -17,13 +17,10 @@
import org.jacodb.ets.model.EtsReturnStmt
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.model.EtsStringType
import org.jacodb.ets.model.EtsType
import org.jacodb.ets.model.EtsUnclearRefType
import org.jacodb.ets.utils.ANONYMOUS_CLASS_PREFIX
import org.jacodb.ets.utils.CONSTRUCTOR_NAME
import org.jacodb.ets.utils.INSTANCE_INIT_METHOD_NAME
import org.jacodb.impl.cfg.graphs.GraphDominators
import org.jacodb.impl.cfg.graphs.findDominators
import org.usvm.dataflow.ifds.ControlEvent
import org.usvm.dataflow.ifds.Edge
import org.usvm.dataflow.ifds.Manager
@@ -52,15 +49,6 @@
private val backwardSummaries = ConcurrentHashMap<EtsMethod, MutableSet<BackwardSummaryAnalyzerEvent>>()
private val forwardSummaries = ConcurrentHashMap<EtsMethod, MutableSet<ForwardSummaryAnalyzerEvent>>()

private val methodDominatorsCache = ConcurrentHashMap<EtsMethod, GraphDominators<EtsStmt>>()

private fun methodDominators(method: EtsMethod): GraphDominators<EtsStmt> =
methodDominatorsCache.computeIfAbsent(method) {
method.flowGraph().findDominators()
}

private val savedTypes: ConcurrentHashMap<EtsType, MutableList<EtsTypeFact>> = ConcurrentHashMap()

private val typeProcessor = TypeFactProcessor(scene = graph.cp)

@OptIn(DelicateCoroutinesApi::class)
@@ -111,7 +99,7 @@
): Map<EtsMethod, Map<AccessPathBase, EtsTypeFact>> {
logger.info { "Preparing backward analysis" }
val backwardGraph = graph.reversed
val backwardAnalyzer = BackwardAnalyzer(backwardGraph, savedTypes, ::methodDominators, doAddKnownTypes)
val backwardAnalyzer = BackwardAnalyzer(backwardGraph, doAddKnownTypes)

val backwardRunner = EtsIfdsRunner(
graph = backwardGraph,
@@ -177,23 +165,12 @@
// }
// }

val typeInfo: Map<EtsType, EtsTypeFact> = savedTypes.mapValues { (type, facts) ->
val typeFact = EtsTypeFact.ObjectEtsTypeFact(type, properties = emptyMap())
facts.fold(typeFact as EtsTypeFact) { acc, it ->
typeProcessor.intersect(acc, it) ?: run {
logger.warn { "Empty intersection type: ${acc.toStringLimited()} & ${it.toStringLimited()}" }
acc
}
}
}

logger.info { "Preparing forward analysis" }
val forwardGraph = graph
val forwardAnalyzer = ForwardAnalyzer(
forwardGraph,
methodTypeScheme,
typeInfo,
doAddKnownTypes,
graph = forwardGraph,
methodInitialTypes = methodTypeScheme,
doAddKnownTypes = doAddKnownTypes,
doAliasAnalysis = doAliasAnalysis,
doLiveVariablesAnalysis = true,
)
@@ -390,12 +367,7 @@

var refined: EtsTypeFact = combinedBackwardType
for ((property, propertyType) in propertyRefinements) {
refined = refined.refineProperty(property, propertyType) ?: this@TypeInferenceManager.run {
logger.warn {
"Empty intersection type: ${refined.toStringLimited()} & ${propertyType.toStringLimited()}"
}
refined
}
refined = refined.refineProperty(property, propertyType)
}

cls.signature to refined
@@ -427,22 +399,45 @@
): Map<AccessPathBase, EtsTypeFact> {
val types = summaries
.mapNotNull { it.exitFact as? BackwardTypeDomainFact.TypedVariable }
.groupBy({ it.variable }, { it.type })
.groupBy({ it.variable.base }, { it.variable.accesses to it.type })
.filter { (base, _) ->
base is AccessPathBase.This
|| base is AccessPathBase.Arg
|| base is AccessPathBase.Static
|| (base is AccessPathBase.Local && base !in getRealLocalsBase(method))
}
.mapValues { (_, typeFacts) ->
typeFacts.reduce { acc, typeFact ->
typeProcessor.intersect(acc, typeFact) ?: run {
logger.warn { "Empty intersection type: ${acc.toStringLimited()} & ${typeFact.toStringLimited()}" }
acc
val propertyRefinements = typeFacts
.groupBy({ it.first }, { it.second })
.mapValues { (accesses, types) ->
val propertyType = types.reduce { acc, t ->
typeProcessor.intersect(acc, t) ?: EtsTypeFact.AnyEtsTypeFact
}
accesses.foldRight(propertyType) { accessor, acc ->
when (accessor) {
is FieldAccessor -> EtsTypeFact.ObjectEtsTypeFact(
cls = null, properties = mapOf(accessor.name to acc)
)

is ElementAccessor -> EtsTypeFact.ArrayEtsTypeFact(acc)
}
}
}
val refined = if (propertyRefinements.keys.any { it.isNotEmpty() }) {
propertyRefinements
.filterKeys { it.isNotEmpty() }
.values
.reduce { acc, typeFact ->
typeProcessor.intersect(acc, typeFact) ?: run {
logger.warn { "Empty intersection type: ${acc.toStringLimited()} & ${typeFact.toStringLimited()}" }
acc
}
}
} else {
EtsTypeFact.AnyEtsTypeFact
}
refined
}

Check warning

Code scanning / detekt

Reports lines with exceeded length Warning

Exceeded max line length (120)

return types
}

@@ -466,26 +461,20 @@
val refinedTypes = facts.mapValues { (base, type) ->
// TODO: throw an error
val typeRefinements = typeFacts[base] ?: return@mapValues type

val propertyRefinements = typeRefinements
.groupBy({ it.variable.accesses }, { it.type })
.mapValues { (_, types) -> types.reduce { acc, t -> typeProcessor.union(acc, t) } }

val rootType = propertyRefinements[emptyList()] ?: run {
if (propertyRefinements.keys.any { it.isNotEmpty() }) {
EtsTypeFact.ObjectEtsTypeFact(null, emptyMap())
} else {
EtsTypeFact.AnyEtsTypeFact
}
}

val refined = rootType.refineProperties(emptyList(), propertyRefinements)

refined
}

typeFacts.let {}

return refinedTypes
}

@@ -556,34 +545,47 @@
pathFromRootObject: List<Accessor>,
typeRefinements: Map<List<Accessor>, EtsTypeFact>,
): EtsTypeFact {
val refinedProperties = properties.mapValues { (propertyName, type) ->
val propertiesRefinements = typeRefinements
.filterKeys { it.startsWith(pathFromRootObject) && it.size > pathFromRootObject.size }
.mapKeys { it.key.drop(pathFromRootObject.size).first() }
.filterKeys { it is FieldAccessor }
.mapKeys { (it.key as FieldAccessor).name }

val properties = this.properties.keys + propertiesRefinements.keys

val refinedProperties = properties.associateWith { propertyName ->
val propertyAccessor = FieldAccessor(propertyName)
val propertyPath = pathFromRootObject + propertyAccessor
val refinedType = typeRefinements[propertyPath]?.let {
typeProcessor.intersect(it, type)
} ?: type // TODO: consider throwing an exception
refinedType.refineProperties(propertyPath, typeRefinements)
val type = this.properties[propertyName]
val typeRefinement = typeRefinements[propertyPath]
val refinedType = if (type != null && typeRefinement != null) {
typeProcessor.intersect(typeRefinement, type)
} else {
type ?: typeRefinement
}
refinedType?.refineProperties(propertyPath, typeRefinements)
?: EtsTypeFact.AnyEtsTypeFact
}
return EtsTypeFact.ObjectEtsTypeFact(cls, refinedProperties)
}

private fun EtsTypeFact.refineProperty(
property: List<Accessor>,
type: EtsTypeFact,
): EtsTypeFact? = when (this) {
is EtsTypeFact.BasicType -> refineProperty(property, type)
): EtsTypeFact = when (this) {
is EtsTypeFact.BasicType -> refineProperty(property, type) ?: EtsTypeFact.AnyEtsTypeFact

is EtsTypeFact.GuardedTypeFact -> this.type.refineProperty(property, type)?.withGuard(guard, guardNegated)
is EtsTypeFact.GuardedTypeFact -> this.type.refineProperty(property, type)

is EtsTypeFact.IntersectionEtsTypeFact -> EtsTypeFact.mkIntersectionType(
types = types.mapTo(hashSetOf()) {
// Note: intersection is empty if any of the types is empty
it.refineProperty(property, type) ?: return null
it.refineProperty(property, type)
}
)

is EtsTypeFact.UnionEtsTypeFact -> EtsTypeFact.mkUnionType(
types = types.mapNotNullTo(hashSetOf()) {
types = types.mapTo(hashSetOf()) {
// Note: ignore empty types in the union
it.refineProperty(property, type)
}
@@ -695,7 +697,7 @@
when (propertyAccessor) {
is FieldAccessor -> {
val propertyType = properties[propertyAccessor.name] ?: return this
val refinedProperty = propertyType.refineProperty(property.drop(1), type) ?: return null
val refinedProperty = propertyType.refineProperty(property.drop(1), type)
val properties = this.properties + (propertyAccessor.name to refinedProperty)
return EtsTypeFact.ObjectEtsTypeFact(cls, properties)
}
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ import org.jacodb.ets.model.EtsLiteralType
import org.jacodb.ets.model.EtsNeverType
import org.jacodb.ets.model.EtsNullType
import org.jacodb.ets.model.EtsNumberType
import org.jacodb.ets.model.EtsRawType
import org.jacodb.ets.model.EtsStringType
import org.jacodb.ets.model.EtsTupleType
import org.jacodb.ets.model.EtsType
@@ -64,6 +65,10 @@ import org.jacodb.ets.model.EtsVoidType
fun EtsType.toDto(): TypeDto = accept(EtsTypeToDto)

private object EtsTypeToDto : EtsType.Visitor<TypeDto> {
override fun visit(type: EtsRawType): TypeDto {
return UnknownTypeDto
}

override fun visit(type: EtsAnyType): TypeDto {
return AnyTypeDto
}
2 changes: 1 addition & 1 deletion usvm-ts-dataflow/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
</encoder>
</appender>

<root level="error">
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
68 changes: 40 additions & 28 deletions usvm-ts-dataflow/src/test/resources/ts/testcases.ts
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ class CaseAssignFieldToLocal1 {
// Case `x := y.f`
class CaseAssignFieldToLocal2 {
entrypoint() {
let y = {f: 42}; // y: { f: number }
let y = { f: 42 }; // y: { f: number }
let x = y.f; // x: number
this.infer(x);
}
@@ -65,22 +65,22 @@ class CaseAssignFieldToSelf {
}

infer(a: any) {
const EXPECTED_ARG_0 = "Object { f: any }"
const EXPECTED_ARG_0 = "Object { f: any }";
}
}

// Case `x.f := x`
class CaseAssignSelfToField {
entrypoint(a: any) {
let x = { f: a }; // x: { f: any }
x.f = x; // x: { f: any }
this.infer(x);
}

infer(a: any) {
const EXPECTED_ARG_0 = "Object { f: any }"
}
}
// class CaseAssignSelfToField {
// entrypoint(a: any) {
// let x = { f: a }; // x: { f: any }
// x.f = x; // x: { f: any }
// this.infer(x);
// }
//
// infer(a: any) {
// const EXPECTED_ARG_0 = "Object { f: any }"
// }
// }

// ----------------------------------------

@@ -581,25 +581,25 @@ class CaseReturnNumber {

// ----------------------------------------

// Case `return arg`
class CaseReturnArgumentNumber {
entrypoint() {
let x = 94; // x: number
this.infer(x);
}
// Case `return arg`
class CaseReturnArgumentNumber {
entrypoint() {
let x = 94; // x: number
this.infer(x);
}

infer(a: any): any {
const EXPECTED_RETURN = "number";
return a;
}
}
infer(a: any): any {
const EXPECTED_RETURN = "number";
return a;
}
}

// ----------------------------------------

// Case `return obj`
class CaseReturnObject {
entrypoint() {
this.infer();
this.infer();
}

infer(): any {
@@ -795,10 +795,22 @@ class CaseNew {
}

infer(a: any): any {
const EXPECTED_ARG_0 = "MyType { f: number }"
const EXPECTED_ARG_0 = "MyType { f: number }";
}
}

// class CaseNew2 {
// entrypoint() {
// let x = new Something(); // unresolved
// console.log(x.foo);
// this.infer(x);
// }
//
// infer(a: any): any {
// const EXPECTED_ARG_0 = "Something { foo: any }";
// }
// }

// ----------------------------------------

// Case `x := number | string`
@@ -929,7 +941,7 @@ class CaseAliasChain1 {
}

infer(a: any): any {
const EXPECTED_ARG_0 = "Object { f: Object { g: number } }"
const EXPECTED_ARG_0 = "Object { f: Object { g: number } }";
}
}

@@ -986,7 +998,7 @@ class CaseFindAssignmentAfterLoop {
x.push(42);
}

this.infer(x)
this.infer(x);
}

infer(a: any) {