diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d5026c497..a82636cfac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,7 @@ jobs: DEST_DIR="arkanalyzer" MAX_RETRIES=10 RETRY_DELAY=3 # Delay between retries in seconds - BRANCH="neo/2025-06-24" + BRANCH="neo/2025-07-18" for ((i=1; i<=MAX_RETRIES; i++)); do git clone --depth=1 --branch $BRANCH $REPO_URL $DEST_DIR && break @@ -177,6 +177,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Show project list + run: ./gradlew projects + - name: Validate Project List run: ./gradlew validateProjectList diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index fc2953acb4..1e312d34fe 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -6,7 +6,7 @@ object Versions { const val clikt = "5.0.0" const val detekt = "1.23.7" const val ini4j = "0.5.4" - const val jacodb = "081adc271e" + const val jacodb = "5acbadfed0" const val juliet = "1.3.2" const val junit = "5.9.3" const val kotlin = "2.1.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index b6b98f4490..6af778f65f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ pluginManagement { plugins { // https://plugins.gradle.org/plugin/com.gradle.develocity - id("com.gradle.develocity") version("4.0.2") + id("com.gradle.develocity") version "4.0.2" } develocity { @@ -55,4 +55,18 @@ findProject(":usvm-python:usvm-python-commons")?.name = "usvm-python-commons" // Actually, `includeBuild("../jacodb")` is enough, but there is a bug in IDEA when path is a symlink. // As a workaround, we convert it to a real absolute path. // See IDEA bug: https://youtrack.jetbrains.com/issue/IDEA-329756 -// includeBuild(file("../jacodb").toPath().toRealPath().toAbsolutePath()) +// val jacodbPath = file("jacodb").takeIf { it.exists() } +// ?: file("../jacodb").takeIf { it.exists() } +// ?: error("Local JacoDB directory not found") +// includeBuild(jacodbPath.toPath().toRealPath().toAbsolutePath()) { +// dependencySubstitution { +// all { +// val requested = requested +// if (requested is ModuleComponentSelector && requested.group == "com.github.UnitTestBot.jacodb") { +// val targetProject = ":${requested.module}" +// useTarget(project(targetProject)) +// logger.info("Substituting ${requested.group}:${requested.module} with $targetProject") +// } +// } +// } +// } diff --git a/usvm-core/src/main/kotlin/org/usvm/Context.kt b/usvm-core/src/main/kotlin/org/usvm/Context.kt index 47d688b98a..8ddaa88a80 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Context.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Context.kt @@ -94,7 +94,7 @@ open class UContext( val addressSort: UAddressSort = mkUninterpretedSort("Address") val nullRef: UNullRef = UNullRef(this) - fun mkNullRef(): USymbolicHeapRef { + open fun mkNullRef(): USymbolicHeapRef { return nullRef } diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt index c62b006dcb..737e34dab3 100644 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt +++ b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt @@ -25,6 +25,7 @@ import org.jacodb.ets.dto.EnumValueTypeDto import org.jacodb.ets.dto.FunctionTypeDto import org.jacodb.ets.dto.GenericTypeDto import org.jacodb.ets.dto.IntersectionTypeDto +import org.jacodb.ets.dto.LexicalEnvTypeDto import org.jacodb.ets.dto.LiteralTypeDto import org.jacodb.ets.dto.LocalSignatureDto import org.jacodb.ets.dto.NeverTypeDto @@ -48,6 +49,7 @@ import org.jacodb.ets.model.EtsEnumValueType import org.jacodb.ets.model.EtsFunctionType import org.jacodb.ets.model.EtsGenericType import org.jacodb.ets.model.EtsIntersectionType +import org.jacodb.ets.model.EtsLexicalEnvType import org.jacodb.ets.model.EtsLiteralType import org.jacodb.ets.model.EtsNeverType import org.jacodb.ets.model.EtsNullType @@ -104,6 +106,14 @@ private object EtsTypeToDto : EtsType.Visitor { ) } + override fun visit(type: EtsLexicalEnvType): TypeDto { + @Suppress("DEPRECATION") + return LexicalEnvTypeDto( + method = type.nestedMethod.toDto(), + closures = type.closures.map { it.toDto() }, + ) + } + override fun visit(type: EtsEnumValueType): TypeDto { return EnumValueTypeDto( signature = type.signature.toDto(), diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt index f52eb1575f..2a788b0605 100644 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt +++ b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt @@ -17,7 +17,10 @@ package org.usvm.dataflow.ts.infer.dto import org.jacodb.ets.dto.ArrayRefDto +import org.jacodb.ets.dto.CaughtExceptionRefDto +import org.jacodb.ets.dto.ClosureFieldRefDto import org.jacodb.ets.dto.ConstantDto +import org.jacodb.ets.dto.GlobalRefDto import org.jacodb.ets.dto.InstanceFieldRefDto import org.jacodb.ets.dto.LocalDto import org.jacodb.ets.dto.ParameterRefDto @@ -26,7 +29,10 @@ import org.jacodb.ets.dto.ThisRefDto import org.jacodb.ets.dto.ValueDto import org.jacodb.ets.model.EtsArrayAccess import org.jacodb.ets.model.EtsBooleanConstant +import org.jacodb.ets.model.EtsCaughtExceptionRef +import org.jacodb.ets.model.EtsClosureFieldRef import org.jacodb.ets.model.EtsConstant +import org.jacodb.ets.model.EtsGlobalRef import org.jacodb.ets.model.EtsInstanceFieldRef import org.jacodb.ets.model.EtsLocal import org.jacodb.ets.model.EtsNullConstant @@ -112,4 +118,25 @@ private object EtsValueToDto : EtsValue.Visitor { field = value.field.toDto(), ) } + + override fun visit(value: EtsCaughtExceptionRef): ValueDto { + return CaughtExceptionRefDto( + type = value.type.toDto(), + ) + } + + override fun visit(value: EtsGlobalRef): ValueDto { + return GlobalRefDto( + name = value.name, + ref = value.ref?.toDto(), + ) + } + + override fun visit(value: EtsClosureFieldRef): ValueDto { + return ClosureFieldRefDto( + base = value.base.toDto(), + fieldName = value.fieldName, + type = value.type.toDto(), + ) + } } diff --git a/usvm-ts/src/main/kotlin/org/usvm/api/TsMock.kt b/usvm-ts/src/main/kotlin/org/usvm/api/TsMock.kt new file mode 100644 index 0000000000..adcff2dd7d --- /dev/null +++ b/usvm-ts/src/main/kotlin/org/usvm/api/TsMock.kt @@ -0,0 +1,38 @@ +package org.usvm.api + +import org.jacodb.ets.model.EtsMethodSignature +import org.jacodb.ets.model.EtsVoidType +import org.usvm.UAddressSort +import org.usvm.UExpr +import org.usvm.machine.expr.TsUnresolvedSort +import org.usvm.machine.interpreter.TsStepScope +import org.usvm.machine.state.TsMethodResult +import org.usvm.machine.types.mkFakeValue + +fun mockMethodCall( + scope: TsStepScope, + method: EtsMethodSignature, +) { + scope.doWithState { + val result: UExpr<*> + if (method.returnType is EtsVoidType) { + result = ctx.mkUndefinedValue() + } else { + val sort = ctx.typeToSort(method.returnType) + result = when (sort) { + is UAddressSort -> makeSymbolicRefUntyped() + + is TsUnresolvedSort -> ctx.mkFakeValue( + scope, + makeSymbolicPrimitive(ctx.boolSort), + makeSymbolicPrimitive(ctx.fp64Sort), + makeSymbolicRefUntyped() + ) + + else -> makeSymbolicPrimitive(sort) + } + } + + methodResult = TsMethodResult.Success.MockedCall(result, method) + } +} diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/TsContext.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/TsContext.kt index f2844f33ee..9a1d5917d0 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/TsContext.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/TsContext.kt @@ -58,11 +58,16 @@ class TsContext( val voidSort: TsVoidSort by lazy { TsVoidSort(this) } val voidValue: TsVoidValue by lazy { TsVoidValue(this) } + @Deprecated("Use mkUndefinedValue() or mkTsNullValue() instead") + override fun mkNullRef(): Nothing { + error("Use mkUndefinedValue() or mkTsNullValue() instead of mkNullRef() in TS context") + } + /** * In TS we treat undefined value as a null reference in other objects. * For real null represented in the language we create another reference. */ - private val undefinedValue: UHeapRef = mkNullRef() + private val undefinedValue: UHeapRef = nullRef fun mkUndefinedValue(): UHeapRef = undefinedValue private val nullValue: UConcreteHeapRef = mkConcreteHeapRef(addressCounter.freshStaticAddress()) @@ -181,7 +186,7 @@ class TsContext( } fun createFakeObjectRef(): UConcreteHeapRef { - val address = mkAddressCounter().freshAllocatedAddress() + MAGIC_OFFSET + val address = addressCounter.freshAllocatedAddress() + MAGIC_OFFSET return mkConcreteHeapRef(address) } @@ -263,6 +268,14 @@ class TsContext( ref } } + + // This is an identifier for a special function representing the 'resolve' function used in promises. + // It is not a real function in the code, but we need it to handle promise resolution. + val resolveFunctionRef: UConcreteHeapRef = allocateConcreteRef() + + // This is an identifier for a special function representing the 'reject' function used in promises. + // It is not a real function in the code, but we need it to handle promise rejection. + val rejectFunctionRef: UConcreteHeapRef = allocateConcreteRef() } class Constants { diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/TsMethodCall.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/TsMethodCall.kt index 428ce13155..49616f7db8 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/TsMethodCall.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/TsMethodCall.kt @@ -7,7 +7,7 @@ import org.jacodb.ets.model.EtsStmtLocation import org.usvm.UExpr sealed interface TsMethodCall : EtsStmt { - val instance: UExpr<*>? + val instance: UExpr<*> val args: List> val returnSite: EtsStmt @@ -21,7 +21,7 @@ sealed interface TsMethodCall : EtsStmt { class TsVirtualMethodCallStmt( val callee: EtsMethodSignature, - override val instance: UExpr<*>?, + override val instance: UExpr<*>, override val args: List>, override val returnSite: EtsStmt, ) : TsMethodCall { @@ -38,7 +38,7 @@ class TsVirtualMethodCallStmt( // and not wrapped in array (if calling a vararg method) class TsConcreteMethodCallStmt( val callee: EtsMethod, - override val instance: UExpr<*>?, + override val instance: UExpr<*>, override val args: List>, override val returnSite: EtsStmt, ) : TsMethodCall { diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt index b778671c2d..b7bb27937f 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt @@ -2,6 +2,7 @@ package org.usvm.machine.expr import io.ksmt.sort.KFp64Sort import io.ksmt.utils.asExpr +import io.ksmt.utils.cast import mu.KotlinLogging import org.jacodb.ets.model.EtsAddExpr import org.jacodb.ets.model.EtsAndExpr @@ -17,8 +18,10 @@ import org.jacodb.ets.model.EtsBitXorExpr import org.jacodb.ets.model.EtsBooleanConstant import org.jacodb.ets.model.EtsBooleanType import org.jacodb.ets.model.EtsCastExpr +import org.jacodb.ets.model.EtsCaughtExceptionRef import org.jacodb.ets.model.EtsClassSignature import org.jacodb.ets.model.EtsClassType +import org.jacodb.ets.model.EtsClosureFieldRef import org.jacodb.ets.model.EtsConstant import org.jacodb.ets.model.EtsDeleteExpr import org.jacodb.ets.model.EtsDivExpr @@ -26,8 +29,8 @@ import org.jacodb.ets.model.EtsEntity import org.jacodb.ets.model.EtsEqExpr import org.jacodb.ets.model.EtsExpExpr import org.jacodb.ets.model.EtsFieldSignature -import org.jacodb.ets.model.EtsFileSignature import org.jacodb.ets.model.EtsFunctionType +import org.jacodb.ets.model.EtsGlobalRef import org.jacodb.ets.model.EtsGtEqExpr import org.jacodb.ets.model.EtsGtExpr import org.jacodb.ets.model.EtsInExpr @@ -35,6 +38,7 @@ import org.jacodb.ets.model.EtsInstanceCallExpr import org.jacodb.ets.model.EtsInstanceFieldRef import org.jacodb.ets.model.EtsInstanceOfExpr import org.jacodb.ets.model.EtsLeftShiftExpr +import org.jacodb.ets.model.EtsLexicalEnvType import org.jacodb.ets.model.EtsLocal import org.jacodb.ets.model.EtsLtEqExpr import org.jacodb.ets.model.EtsLtExpr @@ -79,6 +83,7 @@ import org.jacodb.ets.model.EtsUnsignedRightShiftExpr import org.jacodb.ets.model.EtsValue import org.jacodb.ets.model.EtsVoidExpr import org.jacodb.ets.model.EtsYieldExpr +import org.jacodb.ets.utils.ANONYMOUS_METHOD_PREFIX import org.jacodb.ets.utils.CONSTRUCTOR_NAME import org.jacodb.ets.utils.STATIC_INIT_METHOD_NAME import org.jacodb.ets.utils.UNKNOWN_CLASS_NAME @@ -94,6 +99,7 @@ import org.usvm.USort import org.usvm.api.allocateConcreteRef import org.usvm.api.evalTypeEquals import org.usvm.api.initializeArrayLength +import org.usvm.api.mockMethodCall import org.usvm.dataflow.ts.infer.tryGetKnownType import org.usvm.dataflow.ts.util.type import org.usvm.isAllocatedConcreteHeapRef @@ -119,10 +125,9 @@ import org.usvm.machine.state.localsCount import org.usvm.machine.state.newStmt import org.usvm.machine.types.EtsAuxiliaryType import org.usvm.machine.types.mkFakeValue -import org.usvm.memory.ULValue import org.usvm.sizeSort import org.usvm.util.EtsHierarchy -import org.usvm.util.EtsPropertyResolution +import org.usvm.util.TsResolutionResult import org.usvm.util.createFakeField import org.usvm.util.isResolved import org.usvm.util.mkArrayIndexLValue @@ -392,7 +397,7 @@ class TsExprResolver( } val promiseState = scope.calcOnState { - promiseStates[promise] ?: PromiseState.PENDING + promiseState[promise] ?: PromiseState.PENDING } val isResolved = scope.calcOnState { isResolved(promise) } @@ -402,20 +407,41 @@ class TsExprResolver( "Promise state should be PENDING, but it is $promiseState for $promise" } val executor = scope.calcOnState { - promiseExecutors[promise] + promiseExecutor[promise] ?: error("Await expression should have a promise executor, but it is not set for $promise") } check(executor.cfg.stmts.isNotEmpty()) - scope.doWithState { - // Note: arguments for 'executor' are: - // - `resolve`, if present in parameters - // - `reject`, if present in parameters - // - `promise` == "this", should be the last - check(executor.parameters.size <= 2) { - "Executor should have at most 2 parameters (resolve and reject), but it has ${executor.parameters.size} for $executor" + + val args: MutableList> = mutableListOf() + + // Executor lambda does not have 'this', so we fill it with 'undefined': + args += mkUndefinedValue() + + val params = executor.parameters.toMutableList() + if (params.isNotEmpty() && params[0].type is EtsLexicalEnvType) { + params.removeFirst() + // TODO: handle closures + args += mkUndefinedValue() + } + if (params.isNotEmpty()) { + args += resolveFunctionRef + scope.doWithState { + setBoundThis(resolveFunctionRef, promise) } - val args = executor.parameters.map { mkUndefinedValue() } + promise - // pushSortsForArguments(instance = null, args = emptyList(), localToIdx) + if (params.size >= 2) { + args += rejectFunctionRef + scope.doWithState { + setBoundThis(rejectFunctionRef, promise) + } + if (params.size >= 3) { + error( + "Promise executor should have at most 3 parameters" + + " (closures, resolve, reject), but got ${params.size}" + ) + } + } + } + scope.doWithState { pushSortsForActualArguments(args) memory.stack.push(args.toTypedArray(), executor.localsCount) registerCallee(currentStatement, executor.cfg) @@ -812,149 +838,91 @@ class TsExprResolver( } } - override fun visit(expr: EtsStaticCallExpr): UExpr<*>? = with(ctx) { - if (expr.callee.name == "Number") { - check(expr.args.size == 1) { - "Number() should have exactly one argument, but got ${expr.args.size}" - } - val arg = resolve(expr.args.single()) ?: return null - return mkNumericExpr(arg, scope) + private fun handleR(): UExpr<*>? = with(ctx) { + val mockSymbol = scope.calcOnState { + memory.mocker.createMockSymbol(trackedLiteral = null, addressSort, ownership) } + scope.assert(mkNot(mkEq(mockSymbol, mkTsNullValue()))) + mockSymbol + } - if (expr.callee.name == "Boolean") { - check(expr.args.size == 1) { - "Boolean() should have exactly one argument, but got ${expr.args.size}" - } - val arg = resolve(expr.args.single()) ?: return null - return mkTruthyExpr(arg, scope) + private fun handleNumberConverter(expr: EtsStaticCallExpr): UExpr<*>? = with(ctx) { + check(expr.args.size == 1) { + "Number() should have exactly one argument, but got ${expr.args.size}" } + val arg = resolve(expr.args.single()) ?: return null + return mkNumericExpr(arg, scope) + } - if (expr.callee.name in listOf("resolve", "reject")) { - val promise = resolve( - EtsThis( - EtsClassType( - EtsClassSignature( - "Promise", - EtsFileSignature("typescript", "lib.es5.d.ts") - ) - ) - ) - )?.asExpr(addressSort) ?: return null - check(isAllocatedConcreteHeapRef(promise)) { - "Promise instance should be allocated, but it is not: $promise" - } - val newState = when (expr.callee.name) { - "resolve" -> PromiseState.FULFILLED - "reject" -> PromiseState.REJECTED - else -> error("Unexpected: $expr") - } - check(expr.args.size == 1) { - "${expr.callee.name}() should have exactly one argument, but got ${expr.args.size}" - } - val value = resolve(expr.args.single()) ?: return null - val fakeValue = value.toFakeObject(scope) - scope.doWithState { - markResolved(promise.asExpr(addressSort)) - setPromiseState(promise, newState) - setResolvedValue(promise.asExpr(addressSort), fakeValue) - } - return mkUndefinedValue() + private fun handleBooleanConverter(expr: EtsStaticCallExpr): UExpr<*>? = with(ctx) { + check(expr.args.size == 1) { + "Boolean() should have exactly one argument, but got ${expr.args.size}" } + val arg = resolve(expr.args.single()) ?: return null + return mkTruthyExpr(arg, scope) + } + override fun visit(expr: EtsStaticCallExpr): UExpr<*>? = with(ctx) { + // Mock `$r` calls if (expr.callee.name == "\$r") { - return scope.calcOnState { - val mockSymbol = memory.mocker.createMockSymbol(trackedLiteral = null, addressSort, ownership) - - scope.assert(mkEq(mockSymbol, ctx.mkTsNullValue()).not()) - - mockSymbol - } + return handleR() } - return when (val result = scope.calcOnState { methodResult }) { - is TsMethodResult.Success -> { - scope.doWithState { methodResult = TsMethodResult.NoCall } - result.value - } - - is TsMethodResult.TsException -> { - error("Exception should be handled earlier") - } - - is TsMethodResult.NoCall -> { - val resolutionResult = resolveStaticMethod(expr.callee) - - if (resolutionResult is EtsPropertyResolution.Empty) { - logger.error { "Could not resolve static call: ${expr.callee}" } - scope.assert(falseExpr) - return null - } - - // static virtual call - if (resolutionResult is EtsPropertyResolution.Ambiguous) { - val resolvedArgs = expr.args.map { resolve(it) ?: return null } - - val staticProperties = resolutionResult.properties.take( - Constants.STATIC_METHODS_FORK_LIMIT - ) - - val staticInstances = scope.calcOnState { - staticProperties.map { getStaticInstance(it.enclosingClass!!) } - } - - val concreteCalls = staticProperties.mapIndexed { index, value -> - TsConcreteMethodCallStmt( - callee = value, - instance = staticInstances[index], - args = resolvedArgs, - returnSite = scope.calcOnState { lastStmt } - ) - } + // Handle `Number(...)` calls + if (expr.callee.name == "Number") { + return handleNumberConverter(expr) + } - val blocks = concreteCalls.map { - ctx.mkTrue() to { state: TsState -> state.newStmt(it) } - } + // Handle `Boolean(...)` calls + if (expr.callee.name == "Boolean") { + return handleBooleanConverter(expr) + } - scope.forkMulti(blocks) + val result = scope.calcOnState { methodResult } - return null - } + if (result is TsMethodResult.Success) { + scope.doWithState { methodResult = TsMethodResult.NoCall } + return result.value + } - require(resolutionResult is EtsPropertyResolution.Unique) + if (result is TsMethodResult.TsException) { + error("Exception should be handled earlier") + } - val instance = scope.calcOnState { getStaticInstance(resolutionResult.property.enclosingClass!!) } + check(result is TsMethodResult.NoCall) - val resolvedArgs = expr.args.map { resolve(it) ?: return null } + when (val resolved = resolveStaticMethod(expr.callee)) { + is TsResolutionResult.Empty -> { + logger.error { "Could not resolve static call: ${expr.callee}" } + scope.assert(falseExpr) + } - val concreteCall = TsConcreteMethodCallStmt( - callee = resolutionResult.property, - instance = instance, - args = resolvedArgs, - returnSite = scope.calcOnState { lastStmt }, - ) - scope.doWithState { newStmt(concreteCall) } + is TsResolutionResult.Ambiguous -> { + processAmbiguousStaticMethod(resolved, expr) + } - null + is TsResolutionResult.Unique -> { + processUniqueStaticMethod(resolved, expr) } } + + null } - private fun resolveStaticMethod( - method: EtsMethodSignature, - ): EtsPropertyResolution { + private fun resolveStaticMethod(method: EtsMethodSignature): TsResolutionResult { // Perfect signature: if (method.enclosingClass.name != UNKNOWN_CLASS_NAME) { val classes = hierarchy.classesForType(EtsClassType(method.enclosingClass)) if (classes.size > 1) { val methods = classes.map { it.methods.single { it.name == method.name } } - return EtsPropertyResolution.create(methods) + return TsResolutionResult.create(methods) } - if (classes.isEmpty()) return EtsPropertyResolution.Empty + if (classes.isEmpty()) return TsResolutionResult.Empty val clazz = classes.single() val methods = clazz.methods.filter { it.name == method.name } - return EtsPropertyResolution.create(methods) + return TsResolutionResult.create(methods) } // Unknown signature: @@ -962,13 +930,114 @@ class TsExprResolver( .flatMap { it.methods } .filter { it.name == method.name } - return EtsPropertyResolution.create(methods) + return TsResolutionResult.create(methods) } - override fun visit(expr: EtsPtrCallExpr): UExpr? { - // TODO: IMPORTANT do not forget to fill sorts of arguments map - logger.warn { "visit(${expr::class.simpleName}) is not implemented yet" } - error("Not supported $expr") + private fun processAmbiguousStaticMethod( + resolved: TsResolutionResult.Ambiguous, + expr: EtsStaticCallExpr, + ) { + val resolvedArgs = expr.args.map { resolve(it) ?: return } + val staticProperties = resolved.properties.take(Constants.STATIC_METHODS_FORK_LIMIT) + val staticInstances = scope.calcOnState { + staticProperties.map { getStaticInstance(it.enclosingClass!!) } + } + val concreteCalls = staticProperties.mapIndexed { index, value -> + TsConcreteMethodCallStmt( + callee = value, + instance = staticInstances[index], + args = resolvedArgs, + returnSite = scope.calcOnState { lastStmt } + ) + } + val blocks: List Unit>> = concreteCalls.map { stmt -> + ctx.mkTrue() to { newStmt(stmt) } + } + scope.forkMulti(blocks) + } + + private fun processUniqueStaticMethod( + resolved: TsResolutionResult.Unique, + expr: EtsStaticCallExpr, + ) { + val instance = scope.calcOnState { + getStaticInstance(resolved.property.enclosingClass!!) + } + val args = expr.args.map { resolve(it) ?: return } + val concreteCall = TsConcreteMethodCallStmt( + callee = resolved.property, + instance = instance, + args = args, + returnSite = scope.calcOnState { lastStmt }, + ) + scope.doWithState { newStmt(concreteCall) } + } + + override fun visit(expr: EtsPtrCallExpr): UExpr? = with(ctx) { + when (val result = scope.calcOnState { methodResult }) { + is TsMethodResult.Success -> { + scope.doWithState { methodResult = TsMethodResult.NoCall } + result.value + } + + is TsMethodResult.TsException -> { + error("Exception should be handled earlier") + } + + is TsMethodResult.NoCall -> { + val ptr = resolve(expr.ptr) ?: return null + + if (isAllocatedConcreteHeapRef(ptr)) { + // Handle 'resolve' and 'reject' function call + if (ptr === resolveFunctionRef || ptr === rejectFunctionRef) { + val promise = scope.calcOnState { + boundThis[ptr] ?: error("No bound 'this' for ptr: $ptr") + } + check(isAllocatedConcreteHeapRef(promise)) { + "Promise instance should be allocated, but it is not: $promise" + } + val newState = when (ptr) { + resolveFunctionRef -> PromiseState.FULFILLED + rejectFunctionRef -> PromiseState.REJECTED + else -> error("Unexpected ptr: $ptr") + } + check(expr.args.size == 1) { + "${ + when (ptr) { + resolveFunctionRef -> "resolve" + rejectFunctionRef -> "reject" + else -> error("Unexpected ptr: $ptr") + } + }() should have exactly one argument, but got ${expr.args.size}" + } + val value = resolve(expr.args.single()) ?: return null + val fakeValue = value.toFakeObject(scope) + scope.doWithState { + markResolved(promise.asExpr(addressSort)) + setPromiseState(promise, newState) + setResolvedValue(promise.asExpr(addressSort), fakeValue) + } + return mkUndefinedValue() + } + + val callee = scope.calcOnState { + associatedFunction[ptr] ?: error("No associated methods for ptr: $ptr") + } + val resolvedArgs = expr.args.map { resolve(it) ?: return null } + val concreteCall = TsConcreteMethodCallStmt( + callee = callee.method, + instance = callee.thisInstance ?: ctx.mkUndefinedValue(), + args = resolvedArgs, + returnSite = scope.calcOnState { lastStmt }, + ) + scope.doWithState { newStmt(concreteCall) } + } else { + mockMethodCall(scope, expr.callee) + } + + null + } + } } // endregion @@ -1102,7 +1171,7 @@ class TsExprResolver( val etsField = resolveEtsField(instance, field, hierarchy) val sort = when (etsField) { - is EtsPropertyResolution.Empty -> { + is TsResolutionResult.Empty -> { if (field.name !in listOf("i", "LogLevel")) { logger.warn { "Field $field not found, creating fake field" } } @@ -1114,8 +1183,8 @@ class TsExprResolver( addressSort } - is EtsPropertyResolution.Unique -> typeToSort(etsField.property.type) - is EtsPropertyResolution.Ambiguous -> unresolvedSort + is TsResolutionResult.Unique -> typeToSort(etsField.property.type) + is TsResolutionResult.Ambiguous -> unresolvedSort } scope.doWithState { @@ -1148,7 +1217,7 @@ class TsExprResolver( } // TODO ambiguous enum fields resolution - if (etsField is EtsPropertyResolution.Unique) { + if (etsField is TsResolutionResult.Unique) { val fieldType = etsField.property.type if (fieldType is EtsRawType && fieldType.kind == "EnumValueType") { val fakeType = fakeRef.getFakeType(scope) @@ -1176,61 +1245,89 @@ class TsExprResolver( } } - override fun visit(value: EtsInstanceFieldRef): UExpr? = with(ctx) { - val instanceRefResolved = resolve(value.instance) ?: return null - if (instanceRefResolved.sort != addressSort) { - logger.error("InstanceFieldRef access on not address sort: $instanceRefResolved") - scope.assert(falseExpr) - return null + private fun handleArrayLength( + value: EtsInstanceFieldRef, + instance: UHeapRef, + ): UExpr<*> = with(ctx) { + val arrayType = value.instance.type as EtsArrayType + val length = scope.calcOnState { + val lengthLValue = mkArrayLengthLValue(instance, arrayType) + memory.read(lengthLValue) } - val instanceRef = instanceRefResolved.asExpr(addressSort) - checkUndefinedOrNullPropertyRead(instanceRef) ?: return null + scope.doWithState { + pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0)) + } - // TODO It is a hack for array's length - if (value.field.name == "length") { - if (value.instance.type is EtsArrayType) { - val arrayType = value.instance.type as EtsArrayType - val lengthLValue = mkArrayLengthLValue(instanceRef, arrayType) - val length = scope.calcOnState { memory.read(lengthLValue) } - scope.doWithState { pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0)) } + return mkBvToFpExpr( + fp64Sort, + fpRoundingModeSortDefaultValue(), + length.asExpr(sizeSort), + signed = true, + ) + } - return mkBvToFpExpr(fp64Sort, fpRoundingModeSortDefaultValue(), length.asExpr(sizeSort), signed = true) - } + private fun handleFakeLength( + value: EtsInstanceFieldRef, + instance: UConcreteHeapRef, + ): UExpr<*> = with(ctx) { + val fakeType = instance.getFakeType(scope) - // TODO: handle "length" property for arrays inside fake objects - if (instanceRef.isFakeObject()) { - val fakeType = instanceRef.getFakeType(scope) + // If we want to get length from a fake object, we assume that it is an array. + scope.doWithState { + pathConstraints += fakeType.refTypeExpr + } - // If we want to get length from a fake object, we assume that it is an array. - scope.doWithState { pathConstraints += fakeType.refTypeExpr } + val ref = instance.unwrapRef(scope) - val refLValue = getIntermediateRefLValue(instanceRef.address) - val obj = scope.calcOnState { memory.read(refLValue) } + val arrayType = when (val type = value.instance.type) { + is EtsArrayType -> type - val type = value.instance.type - val arrayType = type as? EtsArrayType ?: run { - check(type is EtsAnyType || type is EtsUnknownType) { - "Expected EtsArrayType, EtsAnyType or EtsUnknownType, but got $type" - } + is EtsAnyType, is EtsUnknownType -> { + // If the type is not an array, we assume it is a fake object with + // a length property that behaves like an array. + EtsArrayType(EtsUnknownType, dimensions = 1) + } - // We don't know the type of the array, since it is a fake object - // If we'd know the type, we would have used it instead of creating a fake object - EtsArrayType(EtsUnknownType, dimensions = 1) - } - val lengthLValue = mkArrayLengthLValue(obj, arrayType) - val length = scope.calcOnState { memory.read(lengthLValue) } + else -> error("Expected EtsArrayType, EtsAnyType or EtsUnknownType, but got $type") + } + val length = scope.calcOnState { + val lengthLValue = mkArrayLengthLValue(ref, arrayType) + memory.read(lengthLValue) + } - scope.doWithState { pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0)) } + scope.doWithState { + pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0)) + } - return mkBvToFpExpr( - fp64Sort, - fpRoundingModeSortDefaultValue(), - length.asExpr(sizeSort), - signed = true - ) + return mkBvToFpExpr( + fp64Sort, + fpRoundingModeSortDefaultValue(), + length.asExpr(sizeSort), + signed = true + ) + } - } + override fun visit(value: EtsInstanceFieldRef): UExpr? = with(ctx) { + val instanceResolved = resolve(value.instance) ?: return null + if (instanceResolved.sort != addressSort) { + logger.error { "Instance of field ref should be a reference, but got $instanceResolved" } + scope.assert(falseExpr) + return null + } + val instanceRef = instanceResolved.asExpr(addressSort) + + checkUndefinedOrNullPropertyRead(instanceRef) ?: return null + + // Handle array length + if (value.field.name == "length" && value.instance.type is EtsArrayType) { + return handleArrayLength(value, instanceRef) + } + + // Handle length property for fake objects + // TODO: handle "length" property for arrays inside fake objects + if (value.field.name == "length" && instanceRef.isFakeObject()) { + return handleFakeLength(value, instanceRef) } return handleFieldRef(value.instance, instanceRef, value.field, hierarchy) @@ -1250,7 +1347,9 @@ class TsExprResolver( scope.doWithState { // TODO: Handle static initializer result val result = methodResult - if (result is TsMethodResult.Success && result.methodSignature() == initializer.signature) { + // TODO: Why this signature check is needed? + // TODO: Why we need to reset methodResult here? Double-check that it is even set anywhere. + if (result is TsMethodResult.Success && result.methodSignature == initializer.signature) { methodResult = TsMethodResult.NoCall } } @@ -1270,6 +1369,32 @@ class TsExprResolver( return handleFieldRef(instance = null, instanceRef, value.field, hierarchy) } + override fun visit(value: EtsCaughtExceptionRef): UExpr? { + logger.warn { "visit(${value::class.simpleName}) is not implemented yet" } + error("Not supported $value") + } + + override fun visit(value: EtsGlobalRef): UExpr? { + logger.warn { "visit(${value::class.simpleName}) is not implemented yet" } + error("Not supported $value") + } + + override fun visit(value: EtsClosureFieldRef): UExpr? = with(ctx) { + val obj = resolve(value.base) ?: return null + check(isAllocatedConcreteHeapRef(obj)) { + "Closure object should be a concrete heap reference, but got $obj" + } + + val sort = typeToSort(value.type) + if (sort is TsUnresolvedSort) { + val lValue = mkFieldLValue(addressSort, obj, value.fieldName) + scope.calcOnState { memory.read(lValue) } + } else { + val lValue = mkFieldLValue(sort, obj, value.fieldName) + scope.calcOnState { memory.read(lValue) } + } + } + // endregion // region OTHER @@ -1355,7 +1480,7 @@ class TsSimpleValueResolver( private val localToIdx: (EtsMethod, EtsValue) -> Int?, ) : EtsValue.Visitor?> { - private fun resolveLocal(local: EtsValue): ULValue<*, USort> { + private fun resolveLocal(local: EtsValue): UExpr<*> = with(ctx) { val currentMethod = scope.calcOnState { lastEnteredMethod } val entrypoint = scope.calcOnState { entrypoint } @@ -1367,17 +1492,40 @@ class TsSimpleValueResolver( if (localIdx == null) { require(local is EtsLocal) + // Handle closures + if (local.name.startsWith("%closures")) { + val existingClosures = scope.calcOnState { closureObject[local.name] } + if (existingClosures != null) { + return existingClosures + } + val type = local.type + check(type is EtsLexicalEnvType) + val obj = allocateConcreteRef() + for (captured in type.closures) { + val resolvedCaptured = resolveLocal(captured) + val lValue = mkFieldLValue(resolvedCaptured.sort, obj, captured.name) + scope.doWithState { + memory.write(lValue, resolvedCaptured.cast(), guard = ctx.trueExpr) + } + } + scope.doWithState { + setClosureObject(local.name, obj) + } + return obj + } + val globalObject = scope.calcOnState { globalObject } val localName = local.name // Check whether this local was already created or not if (localName in scope.calcOnState { addedArtificialLocals }) { val sort = ctx.typeToSort(local.type) - if (sort is TsUnresolvedSort) { - return mkFieldLValue(ctx.addressSort, globalObject, local.name) + val lValue = if (sort is TsUnresolvedSort) { + mkFieldLValue(ctx.addressSort, globalObject, local.name) } else { - return mkFieldLValue(sort, globalObject, local.name) + mkFieldLValue(sort, globalObject, local.name) } + return scope.calcOnState { memory.read(lValue) } } logger.warn { "Cannot resolve local $local, creating a field of the global object" } @@ -1387,12 +1535,13 @@ class TsSimpleValueResolver( } val sort = ctx.typeToSort(local.type) - if (sort is TsUnresolvedSort) { + val lValue = if (sort is TsUnresolvedSort) { globalObject.createFakeField(localName, scope) - return mkFieldLValue(ctx.addressSort, globalObject, local.name) + mkFieldLValue(ctx.addressSort, globalObject, local.name) } else { - return mkFieldLValue(sort, globalObject, local.name) + mkFieldLValue(sort, globalObject, local.name) } + return scope.calcOnState { memory.read(lValue) } } val sort = scope.calcOnState { @@ -1403,33 +1552,42 @@ class TsSimpleValueResolver( // If we are not in the entrypoint, all correct values are already resolved and we can just return // a registerStackLValue for the local if (currentMethod != entrypoint) { - return mkRegisterStackLValue(sort, localIdx) + val lValue = mkRegisterStackLValue(sort, localIdx) + return scope.calcOnState { memory.read(lValue) } } // arguments and this for the first stack frame - return when (sort) { - is UBoolSort -> mkRegisterStackLValue(sort, localIdx) - is KFp64Sort -> mkRegisterStackLValue(sort, localIdx) - is UAddressSort -> mkRegisterStackLValue(sort, localIdx) + when (sort) { + is UBoolSort -> { + val lValue = mkRegisterStackLValue(sort, localIdx) + scope.calcOnState { memory.read(lValue) } + } + + is KFp64Sort -> { + val lValue = mkRegisterStackLValue(sort, localIdx) + scope.calcOnState { memory.read(lValue) } + } + + is UAddressSort -> { + val lValue = mkRegisterStackLValue(sort, localIdx) + scope.calcOnState { memory.read(lValue) } + } + is TsUnresolvedSort -> { check(local is EtsThis || local is EtsParameterRef) { "Only This and ParameterRef are expected here" } - val lValue = mkRegisterStackLValue(ctx.addressSort, localIdx) - val boolRValue = ctx.mkRegisterReading(localIdx, ctx.boolSort) val fpRValue = ctx.mkRegisterReading(localIdx, ctx.fp64Sort) val refRValue = ctx.mkRegisterReading(localIdx, ctx.addressSort) val fakeObject = ctx.mkFakeValue(scope, boolRValue, fpRValue, refRValue) + val lValue = mkRegisterStackLValue(ctx.addressSort, localIdx) scope.calcOnState { - with(ctx) { - memory.write(lValue, fakeObject.asExpr(addressSort), guard = trueExpr) - } + memory.write(lValue, fakeObject.asExpr(ctx.addressSort), guard = ctx.trueExpr) } - - lValue + fakeObject } else -> error("Unsupported sort $sort") @@ -1444,6 +1602,19 @@ class TsSimpleValueResolver( return ctx.mkFpInf(false, ctx.fp64Sort) } + if (local.name.startsWith(ANONYMOUS_METHOD_PREFIX)) { + val sig = EtsMethodSignature( + enclosingClass = EtsClassSignature.UNKNOWN, + name = local.name, + parameters = emptyList(), + returnType = EtsUnknownType, + ) + val methods = ctx.resolveEtsMethods(sig) + val method = methods.single() + val ref = scope.calcOnState { getMethodRef(method) } + return ref + } + val currentMethod = scope.calcOnState { lastEnteredMethod } if (local !in currentMethod.getDeclaredLocals()) { if (local.type is EtsFunctionType) { @@ -1452,18 +1623,15 @@ class TsSimpleValueResolver( } } - val lValue = resolveLocal(local) - return scope.calcOnState { memory.read(lValue) } + return resolveLocal(local) } override fun visit(value: EtsParameterRef): UExpr { - val lValue = resolveLocal(value) - return scope.calcOnState { memory.read(lValue) } + return resolveLocal(value) } override fun visit(value: EtsThis): UExpr { - val lValue = resolveLocal(value) - return scope.calcOnState { memory.read(lValue) } + return resolveLocal(value) } override fun visit(value: EtsConstant): UExpr = with(ctx) { @@ -1502,4 +1670,16 @@ class TsSimpleValueResolver( override fun visit(value: EtsStaticFieldRef): UExpr = with(ctx) { error("Should not be called") } + + override fun visit(value: EtsCaughtExceptionRef): UExpr? { + error("Should not be called") + } + + override fun visit(value: EtsGlobalRef): UExpr? { + error("Should not be called") + } + + override fun visit(value: EtsClosureFieldRef): UExpr? { + error("Should not be called") + } } diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsFunction.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsFunction.kt new file mode 100644 index 0000000000..928ffccbfa --- /dev/null +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsFunction.kt @@ -0,0 +1,9 @@ +package org.usvm.machine.interpreter + +import org.jacodb.ets.model.EtsMethod +import org.usvm.UHeapRef + +class TsFunction( + val method: EtsMethod, + val thisInstance: UHeapRef?, +) diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt index b89d9156d6..463aa09664 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt @@ -13,12 +13,10 @@ import org.jacodb.ets.model.EtsIfStmt import org.jacodb.ets.model.EtsInstanceFieldRef import org.jacodb.ets.model.EtsLocal import org.jacodb.ets.model.EtsMethod -import org.jacodb.ets.model.EtsMethodSignature import org.jacodb.ets.model.EtsNopStmt import org.jacodb.ets.model.EtsNullType import org.jacodb.ets.model.EtsNumberType import org.jacodb.ets.model.EtsParameterRef -import org.jacodb.ets.model.EtsPtrCallExpr import org.jacodb.ets.model.EtsRefType import org.jacodb.ets.model.EtsReturnStmt import org.jacodb.ets.model.EtsStaticFieldRef @@ -31,20 +29,17 @@ import org.jacodb.ets.model.EtsUndefinedType import org.jacodb.ets.model.EtsUnionType import org.jacodb.ets.model.EtsUnknownType import org.jacodb.ets.model.EtsValue -import org.jacodb.ets.model.EtsVoidType import org.jacodb.ets.utils.CONSTRUCTOR_NAME import org.jacodb.ets.utils.callExpr import org.jacodb.ets.utils.getDeclaredLocals import org.usvm.StepResult import org.usvm.StepScope -import org.usvm.UAddressSort import org.usvm.UExpr import org.usvm.UInterpreter import org.usvm.UIteExpr import org.usvm.api.evalTypeEquals import org.usvm.api.initializeArray -import org.usvm.api.makeSymbolicPrimitive -import org.usvm.api.makeSymbolicRefUntyped +import org.usvm.api.mockMethodCall import org.usvm.api.targets.TsTarget import org.usvm.api.typeStreamOf import org.usvm.collections.immutable.internal.MutabilityOwnership @@ -67,13 +62,12 @@ import org.usvm.machine.state.newStmt import org.usvm.machine.state.parametersWithThisCount import org.usvm.machine.state.returnValue import org.usvm.machine.types.EtsAuxiliaryType -import org.usvm.machine.types.mkFakeValue import org.usvm.machine.types.toAuxiliaryType import org.usvm.sizeSort import org.usvm.targets.UTargetsSet import org.usvm.types.TypesResult import org.usvm.types.single -import org.usvm.util.EtsPropertyResolution +import org.usvm.util.TsResolutionResult import org.usvm.util.mkArrayIndexLValue import org.usvm.util.mkArrayLengthLValue import org.usvm.util.mkFieldLValue @@ -156,7 +150,7 @@ class TsInterpreter( // NOTE: USE '.callee' INSTEAD OF '.method' !!! - val instance = requireNotNull(stmt.instance) { "Virtual code invocation with null as an instance" } + val instance = stmt.instance val unwrappedInstance = if (instance.isFakeObject()) { // TODO support primitives calls @@ -179,7 +173,7 @@ class TsInterpreter( if (stmt.callee.name == CONSTRUCTOR_NAME) { // Approximate unresolved constructor: scope.doWithState { - methodResult = TsMethodResult.Success.MockedCall(stmt.callee, unwrappedInstance) + methodResult = TsMethodResult.Success.MockedCall(unwrappedInstance, stmt.callee) newStmt(stmt.returnSite) } return @@ -231,9 +225,7 @@ class TsInterpreter( if (possibleTypesSet.singleOrNull() == EtsAnyType) { mockMethodCall(scope, stmt.callee) - scope.calcOnState { - newStmt(stmt.returnSite) - } + scope.doWithState { newStmt(stmt.returnSite) } return } @@ -297,17 +289,17 @@ class TsInterpreter( } constraint to block } + if (conditionsWithBlocks.isEmpty()) { logger.warn { "No suitable methods found for call: ${stmt.callee} with instance: $unwrappedInstance" } mockMethodCall(scope, stmt.callee) - scope.doWithState { - newStmt(stmt.returnSite) - } - } else { - scope.forkMulti(conditionsWithBlocks) + scope.doWithState { newStmt(stmt.returnSite) } + return } + + scope.forkMulti(conditionsWithBlocks) } private fun visitConcreteMethodCall(scope: TsStepScope, stmt: TsConcreteMethodCallStmt) { @@ -318,9 +310,7 @@ class TsInterpreter( if (stmt.callee.signature.enclosingClass.name == "Log") { mockMethodCall(scope, stmt.callee.signature) - scope.doWithState { - newStmt(stmt.returnSite) - } + scope.doWithState { newStmt(stmt.returnSite) } return } @@ -330,9 +320,7 @@ class TsInterpreter( // If the method doesn't have entry points, // we go through it, we just mock the call mockMethodCall(scope, stmt.callee.signature) - scope.doWithState { - newStmt(stmt.returnSite) - } + scope.doWithState { newStmt(stmt.returnSite) } return } @@ -343,6 +331,8 @@ class TsInterpreter( val numActual = stmt.args.size val numFormal = stmt.callee.parameters.size + args += stmt.instance + // vararg call: // function f(x: any, ...args: any[]) {} // f(1, 2, 3) -> f(1, [2, 3]) @@ -401,12 +391,10 @@ class TsInterpreter( } } - check(args.size == numFormal) { - "Expected $numFormal arguments, got ${args.size}" + check(args.size - 1 == numFormal) { + "Expected $numFormal arguments, got ${args.size - 1} (not counting 'this')" } - args += stmt.instance!! - // TODO: re-check push sorts for arguments pushSortsForActualArguments(args) callStack.push(stmt.callee, stmt.returnSite) @@ -471,13 +459,14 @@ class TsInterpreter( private fun visitAssignStmt(scope: TsStepScope, stmt: EtsAssignStmt) = with(ctx) { val exprResolver = exprResolverWithScope(scope) - stmt.callExpr?.let { + val callExpr = stmt.callExpr + if (callExpr != null) { val methodResult = scope.calcOnState { methodResult } when (methodResult) { is TsMethodResult.NoCall -> observer?.onCallWithUnresolvedArguments( exprResolver.simpleValueResolver, - it, + callExpr, scope ) @@ -490,16 +479,14 @@ class TsInterpreter( is TsMethodResult.TsException -> error("Exceptions must be processed earlier") } - if (it is EtsPtrCallExpr) { - mockMethodCall(scope, it.callee) - return - } - if (!tsOptions.interproceduralAnalysis && methodResult == TsMethodResult.NoCall) { - mockMethodCall(scope, it.callee) + mockMethodCall(scope, callExpr.callee) + scope.doWithState { newStmt(stmt) } return } - } ?: observer?.onAssignStatement(exprResolver.simpleValueResolver, stmt, scope) + } else { + observer?.onAssignStatement(exprResolver.simpleValueResolver, stmt, scope) + } val expr = exprResolver.resolve(stmt.rhv) ?: return @@ -597,9 +584,9 @@ class TsInterpreter( // If there is no such field, we create a fake field for the expr val sort = when (etsField) { - is EtsPropertyResolution.Empty -> unresolvedSort - is EtsPropertyResolution.Unique -> typeToSort(etsField.property.type) - is EtsPropertyResolution.Ambiguous -> unresolvedSort + is TsResolutionResult.Empty -> unresolvedSort + is TsResolutionResult.Unique -> typeToSort(etsField.property.type) + is TsResolutionResult.Ambiguous -> unresolvedSort } if (sort == unresolvedSort) { @@ -692,13 +679,11 @@ class TsInterpreter( if (scope.calcOnState { methodResult } is TsMethodResult.Success) { scope.doWithState { methodResult = TsMethodResult.NoCall - newStmt(stmt.nextStmt!!) } - return - } - - if (stmt.expr is EtsPtrCallExpr) { - mockMethodCall(scope, stmt.expr.callee) + val nextStmt = stmt.nextStmt ?: return + scope.doWithState { + newStmt(nextStmt) + } return } @@ -750,11 +735,11 @@ class TsInterpreter( map[local.name] } - // Note: 'this' has index 'n' - is EtsThis -> method.parameters.size + // Note: 'this' has index 0 + is EtsThis -> 0 - // Note: arguments have indices from 0 to (n-1) - is EtsParameterRef -> local.index + // Note: arguments have indices from 1 to n + is EtsParameterRef -> local.index + 1 else -> error("Unexpected local: $local") } @@ -774,6 +759,7 @@ class TsInterpreter( // TODO check for statics val thisIdx = mapLocalToIdx(method, EtsThis(method.enclosingClass!!.type)) ?: error("Cannot find index for 'this' in method: $method") + check(thisIdx == 0) val thisInstanceRef = mkRegisterStackLValue(addressSort, thisIdx) val thisRef = state.memory.read(thisInstanceRef).asExpr(addressSort) @@ -784,14 +770,16 @@ class TsInterpreter( state.pathConstraints += state.memory.types.evalTypeEquals(thisRef, method.enclosingClass!!.type) method.parameters.forEachIndexed { i, param -> + val idx = i + 1 // +1 because 0 is reserved for `this` + val ref by lazy { - val lValue = mkRegisterStackLValue(addressSort, i) + val lValue = mkRegisterStackLValue(addressSort, idx) state.memory.read(lValue).asExpr(addressSort) } val parameterType = param.type if (parameterType is EtsRefType) { - val argLValue = mkRegisterStackLValue(addressSort, i) + val argLValue = mkRegisterStackLValue(addressSort, idx) val ref = state.memory.read(argLValue).asExpr(addressSort) if (parameterType is EtsArrayType) { state.pathConstraints += state.memory.types.evalTypeEquals(ref, parameterType) @@ -841,30 +829,6 @@ class TsInterpreter( state } - private fun mockMethodCall(scope: TsStepScope, method: EtsMethodSignature) { - scope.doWithState { - if (method.returnType is EtsVoidType) { - methodResult = TsMethodResult.Success.MockedCall(method, ctx.mkUndefinedValue()) - return@doWithState - } - - val resultSort = ctx.typeToSort(method.returnType) - val resultValue = when (resultSort) { - is UAddressSort -> makeSymbolicRefUntyped() - - is TsUnresolvedSort -> ctx.mkFakeValue( - scope, - makeSymbolicPrimitive(ctx.boolSort), - makeSymbolicPrimitive(ctx.fp64Sort), - makeSymbolicRefUntyped() - ) - - else -> makeSymbolicPrimitive(resultSort) - } - methodResult = TsMethodResult.Success.MockedCall(method, resultValue) - } - } - // TODO: expand with interpreter implementation private val EtsStmt.nextStmt: EtsStmt? get() = graph.successors(this).firstOrNull() diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsMethodResult.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsMethodResult.kt index 1bd8f7acb3..27a33458b2 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsMethodResult.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsMethodResult.kt @@ -5,7 +5,6 @@ import org.jacodb.ets.model.EtsMethodSignature import org.jacodb.ets.model.EtsType import org.usvm.UExpr import org.usvm.UHeapRef -import org.usvm.USort /** * Represents a result of a method invocation. @@ -16,25 +15,24 @@ sealed interface TsMethodResult { */ object NoCall : TsMethodResult - sealed class Success(val value: UExpr) : TsMethodResult { - abstract fun methodSignature(): EtsMethodSignature + sealed interface Success : TsMethodResult { + val value: UExpr<*> + val methodSignature: EtsMethodSignature /** * A [method] successfully returned a [value]. */ class RegularCall( + override val value: UExpr<*>, val method: EtsMethod, - value: UExpr, - ) : Success(value) { - override fun methodSignature(): EtsMethodSignature = method.signature + ) : Success { + override val methodSignature: EtsMethodSignature get() = method.signature } class MockedCall( - val methodSignature: EtsMethodSignature, - value: UExpr, - ) : Success(value) { - override fun methodSignature(): EtsMethodSignature = methodSignature - } + override val value: UExpr<*>, + override val methodSignature: EtsMethodSignature, + ) : Success } /** diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsState.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsState.kt index 87649e8366..1de4a84b83 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsState.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsState.kt @@ -13,8 +13,10 @@ import org.usvm.PathNode import org.usvm.UCallStack import org.usvm.UConcreteHeapRef import org.usvm.UExpr +import org.usvm.UHeapRef import org.usvm.USort import org.usvm.UState +import org.usvm.api.allocateConcreteRef import org.usvm.api.targets.TsTarget import org.usvm.collections.immutable.getOrPut import org.usvm.collections.immutable.implementations.immutableMap.UPersistentHashMap @@ -23,6 +25,7 @@ import org.usvm.collections.immutable.persistentHashMapOf import org.usvm.constraints.UPathConstraints import org.usvm.machine.TsContext import org.usvm.machine.interpreter.PromiseState +import org.usvm.machine.interpreter.TsFunction import org.usvm.memory.ULValue import org.usvm.memory.UMemory import org.usvm.model.UModelBase @@ -55,8 +58,12 @@ class TsState( val addedArtificialLocals: MutableSet = hashSetOf(), val lValuesToAllocatedFakeObjects: MutableList, UConcreteHeapRef>> = mutableListOf(), var discoveredCallees: UPersistentHashMap, EtsBlockCfg> = persistentHashMapOf(), - var promiseStates: UPersistentHashMap = persistentHashMapOf(), - var promiseExecutors: UPersistentHashMap = persistentHashMapOf(), + var promiseState: UPersistentHashMap = persistentHashMapOf(), + var promiseExecutor: UPersistentHashMap = persistentHashMapOf(), + var methodToRef: UPersistentHashMap = persistentHashMapOf(), + var associatedFunction: UPersistentHashMap = persistentHashMapOf(), + var closureObject: UPersistentHashMap = persistentHashMapOf(), + var boundThis: UPersistentHashMap = persistentHashMapOf(), ) : UState( ctx = ctx, initOwnership = ownership, @@ -120,18 +127,18 @@ class TsState( // Note: first, push an empty map, then fill the arguments, and then the instance (this) pushLocalToSortStack() - argSorts.forEachIndexed { index, sort -> - saveSortForLocal(index, sort) + instanceSort?.let { saveSortForLocal(0, it) } + argSorts.forEachIndexed { i, sort -> + val idx = i + 1 // + 1 because 0 is reserved for `this` + saveSortForLocal(idx, sort) } - instanceSort?.let { saveSortForLocal(args.size, it) } } fun pushSortsForActualArguments( arguments: List>, ) { pushLocalToSortStack() - arguments.forEachIndexed { index, arg -> - val idx = index + arguments.forEachIndexed { idx, arg -> saveSortForLocal(idx, arg.sort) } } @@ -148,14 +155,38 @@ class TsState( promise: UConcreteHeapRef, state: PromiseState, ) { - promiseStates = promiseStates.put(promise, state, ownership) + promiseState = promiseState.put(promise, state, ownership) } fun setPromiseExecutor( promise: UConcreteHeapRef, method: EtsMethod, ) { - promiseExecutors = promiseExecutors.put(promise, method, ownership) + promiseExecutor = promiseExecutor.put(promise, method, ownership) + } + + fun getMethodRef( + method: EtsMethod, + thisInstance: UHeapRef? = null, + ): UConcreteHeapRef { + val (updated, result) = methodToRef.getOrPut(method, ownership) { ctx.allocateConcreteRef() } + associatedFunction = associatedFunction.put(result, TsFunction(method, thisInstance), ownership) + methodToRef = updated + return result + } + + fun setClosureObject( + name: String, + closure: UConcreteHeapRef, + ) { + closureObject = closureObject.put(name, closure, ownership) + } + + fun setBoundThis( + instance: UConcreteHeapRef, + thisRef: UHeapRef, + ) { + boundThis = boundThis.put(instance, thisRef, ownership) } override fun clone(newConstraints: UPathConstraints?): TsState { @@ -185,8 +216,11 @@ class TsState( addedArtificialLocals = addedArtificialLocals, lValuesToAllocatedFakeObjects = lValuesToAllocatedFakeObjects.toMutableList(), discoveredCallees = discoveredCallees, - promiseStates = promiseStates, - promiseExecutors = promiseExecutors, + promiseState = promiseState, + promiseExecutor = promiseExecutor, + methodToRef = methodToRef, + associatedFunction = associatedFunction, + closureObject = closureObject, ) } diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsStateUtils.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsStateUtils.kt index e706047b5f..2a639ca915 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsStateUtils.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/state/TsStateUtils.kt @@ -6,7 +6,8 @@ import org.jacodb.ets.utils.getDeclaredLocals import org.usvm.UExpr import org.usvm.USort -val TsState.lastStmt get() = pathNode.statement +val TsState.lastStmt: EtsStmt + get() = currentStatement fun TsState.newStmt(stmt: EtsStmt) { pathNode += stmt @@ -20,7 +21,7 @@ fun TsState.returnValue(valueToReturn: UExpr) { popLocalToSortStack() } - methodResult = TsMethodResult.Success.RegularCall(returnFromMethod, valueToReturn) + methodResult = TsMethodResult.Success.RegularCall(valueToReturn, returnFromMethod) if (returnSite != null) { newStmt(returnSite) diff --git a/usvm-ts/src/main/kotlin/org/usvm/util/EtsFieldResolver.kt b/usvm-ts/src/main/kotlin/org/usvm/util/EtsFieldResolver.kt index 695e79e21c..5188459900 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/util/EtsFieldResolver.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/util/EtsFieldResolver.kt @@ -16,7 +16,7 @@ fun TsContext.resolveEtsField( instance: EtsLocal?, field: EtsFieldSignature, hierarchy: EtsHierarchy, -): EtsPropertyResolution { +): TsResolutionResult { // Perfect signature: if (field.enclosingClass.name != UNKNOWN_CLASS_NAME) { val classes = hierarchy.classesForType(EtsClassType(field.enclosingClass)) @@ -29,7 +29,7 @@ fun TsContext.resolveEtsField( val clazz = classes.single() val fields = clazz.getAllFields(hierarchy).filter { it.name == field.name } if (fields.size == 1) { - return EtsPropertyResolution.create(fields.single()) + return TsResolutionResult.create(fields.single()) } } @@ -39,12 +39,12 @@ fun TsContext.resolveEtsField( when (instanceType) { is EtsClassType -> { val field = tryGetSingleField(scene, instanceType.signature.name, field.name, hierarchy) - if (field != null) return EtsPropertyResolution.create(field) + if (field != null) return TsResolutionResult.create(field) } is EtsUnclearRefType -> { val field = tryGetSingleField(scene, instanceType.typeName, field.name, hierarchy) - if (field != null) return EtsPropertyResolution.create(field) + if (field != null) return TsResolutionResult.create(field) } } } @@ -53,7 +53,7 @@ fun TsContext.resolveEtsField( cls.getAllFields(hierarchy).filter { it.name == field.name } } - return EtsPropertyResolution.create(fields) + return TsResolutionResult.create(fields) } private fun tryGetSingleField( @@ -116,15 +116,15 @@ fun EtsClass.getAllProperties(hierarchy: EtsHierarchy): Pair, return allFields to allMethods } -sealed class EtsPropertyResolution { - data class Unique(val property: T) : EtsPropertyResolution() - data class Ambiguous(val properties: List) : EtsPropertyResolution() - data object Empty : EtsPropertyResolution() +sealed class TsResolutionResult { + data class Unique(val property: T) : TsResolutionResult() + data class Ambiguous(val properties: List) : TsResolutionResult() + data object Empty : TsResolutionResult() companion object { fun create(property: T) = Unique(property) - fun create(properties: List): EtsPropertyResolution { + fun create(properties: List): TsResolutionResult { return when { properties.isEmpty() -> Empty properties.size == 1 -> Unique(properties.single()) diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/Async.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/Async.kt index 618e8d2a5c..c51dbb8ca6 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/Async.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/Async.kt @@ -3,34 +3,29 @@ package org.usvm.samples import org.jacodb.ets.model.EtsScene import org.usvm.api.TsTestValue import org.usvm.util.TsMethodTestRunner +import org.usvm.util.eq import kotlin.test.Test class Async : TsMethodTestRunner() { - companion object { - private const val SDK_TYPESCRIPT_PATH = "/sdk/typescript" - private const val SDK_OHOS_PATH = "/sdk/ohos" - } - private val className = this::class.simpleName!! override val scene: EtsScene = loadSampleScene(className) @Test - fun `create and await promise`() { - val method = getMethod(className, "createAndAwaitPromise") + fun `await resolving promise`() { + val method = getMethod(className, "awaitResolvingPromise") discoverProperties( method = method, - { r -> r.number == 1.0 }, invariants = arrayOf( - { r -> r.number != -1.0 }, + { r -> r.number eq 42 }, ) ) } @Test - fun `create and await rejecting promise`() { - val method = getMethod(className, "createAndAwaitRejectingPromise") + fun `await rejecting promise`() { + val method = getMethod(className, "awaitRejectingPromise") discoverProperties( method = method, { r -> r is TsTestValue.TsException }, @@ -43,7 +38,7 @@ class Async : TsMethodTestRunner() { discoverProperties( method = method, invariants = arrayOf( - { r -> r.number == 50.0 }, + { r -> r.number eq 42 }, ) ) } diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/Call.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/Call.kt index 4a0854c08d..cacf823550 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/Call.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/Call.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.usvm.api.TsTestValue import org.usvm.util.TsMethodTestRunner +import org.usvm.util.eq class Call : TsMethodTestRunner() { @@ -288,13 +289,128 @@ class Call : TsMethodTestRunner() { { r -> r.number == 20.0 }, ) } -} -fun fib(n: Double): Double { - if (n.isNaN()) return 0.0 - if (n < 0) return -1.0 - if (n > 10) return -100.0 - if (n == 0.0) return 1.0 - if (n == 1.0) return 1.0 - return fib(n - 1.0) + fib(n - 2.0) + @Test + fun `test call lambda`() { + val method = getMethod(className, "callLambda") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number == 42.0 }, + ) + ) + } + + @Test + fun `test call closure capturing local`() { + val method = getMethod(className, "callClosureCapturingLocal") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number == 42.0 }, + ) + ) + } + + @Test + fun `test call closure capturing arguments`() { + val method = getMethod(className, "callClosureCapturingArguments") + discoverProperties( + method = method, + { a, b, r -> + (a.value && b.value) && r.number == 1.0 + }, + { a, b, r -> + !(a.value && b.value) && r.number == 2.0 + }, + invariants = arrayOf( + { _, _, r -> r.number in listOf(1.0, 2.0) }, + ) + ) + } + + @Test + fun `test call nested lambda`() { + val method = getMethod(className, "callNestedLambda") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number == 42.0 }, + ) + ) + } + + @Test + fun `test call nested closure capturing outer local`() { + val method = getMethod(className, "callNestedClosureCapturingOuterLocal") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number == 42.0 }, + ) + ) + } + + @Test + fun `test call nested closure capturing inner local`() { + val method = getMethod(className, "callNestedClosureCapturingInnerLocal") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number == 42.0 }, + ) + ) + } + + @Test + fun `test call nested closure capturing local and argument`() { + val method = getMethod(className, "callNestedClosureCapturingLocalAndArgument") + discoverProperties( + method = method, + { a, r -> a.value && r.number == 1.0 }, + { a, r -> !a.value && r.number == 2.0 }, + invariants = arrayOf( + { _, r -> r.number in listOf(1.0, 2.0) } + ) + ) + } + + @Disabled("Capturing mutable locals is not properly supported in ArkIR") + // Note: This test is disabled because ArkIR cannot properly represent the mutation + // of a captured mutable local (`let` or `var`) inside a closure. + // Due to this, `x += 100` instruction has no effect, and the result is 145 (120+20) instead of 225 (120+125). + // A possible solution would be to represent LHS in `x += 100` with `ClosureFieldRef` instead of `Local`. + @Test + fun `test call closure capturing mutable local`() { + val method = getMethod(className, "callClosureCapturingMutableLocal") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number eq 245 }, + ) + ) + } + + @Disabled("Capturing mutable locals is not properly supported in ArkIR") + // Note: See above. + // This test incorrectly produces 20 instead of 120. + @Test + fun `test call closure mutating captured local`() { + val method = getMethod(className, "callClosureMutatingCapturedLocal") + discoverProperties( + method = method, + invariants = arrayOf( + { r -> r.number eq 120 }, + ) + ) + } + + private fun fib(n: Double): Double { + if (n.isNaN()) return 0.0 + if (n < 0) return -1.0 + if (n > 10) return -100.0 + if (n == 0.0) return 1.0 + if (n == 1.0) return 1.0 + return fib(n - 1.0) + fib(n - 2.0) + } } diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/operators/Division.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/operators/Division.kt index df730b468c..6244a87621 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/operators/Division.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/operators/Division.kt @@ -1,6 +1,7 @@ package org.usvm.samples.operators import org.jacodb.ets.model.EtsScene +import org.junit.jupiter.api.Disabled import org.usvm.api.TsTestValue import org.usvm.util.TsMethodTestRunner import org.usvm.util.toDouble @@ -49,6 +50,7 @@ class Division : TsMethodTestRunner() { ) } + @Disabled("Long running test") @Test fun testUnknownDivision() { val method = getMethod(className, "unknownDivision") diff --git a/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt b/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt index 31d2cfdfa8..c6e7c5da2c 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt @@ -314,7 +314,8 @@ open class TsTestStateResolver( } fun resolveParameters(): List = with(ctx) { - method.parameters.mapIndexed { idx, param -> + method.parameters.mapIndexed { i, param -> + val idx = i + 1 // +1 because the register 0 is reserved for `this` val sort = typeToSort(param.type) if (sort is TsUnresolvedSort) { diff --git a/usvm-ts/src/test/kotlin/org/usvm/util/Util.kt b/usvm-ts/src/test/kotlin/org/usvm/util/Util.kt index 0bb0ca1e10..056dae2934 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/util/Util.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/util/Util.kt @@ -1,3 +1,13 @@ package org.usvm.util +import kotlin.math.abs + fun Boolean.toDouble() = if (this) 1.0 else 0.0 + +infix fun Double.eq(other: Int): Boolean { + return this eq other.toDouble() +} + +infix fun Double.eq(other: Double): Boolean { + return abs(this - other) < 1e-9 +} diff --git a/usvm-ts/src/test/resources/samples/Async.ts b/usvm-ts/src/test/resources/samples/Async.ts index 8f2d67c70c..ac3a966384 100644 --- a/usvm-ts/src/test/resources/samples/Async.ts +++ b/usvm-ts/src/test/resources/samples/Async.ts @@ -2,19 +2,14 @@ // noinspection JSUnusedGlobalSymbols class Async { - createAndAwaitPromise(): number { + awaitResolvingPromise(): number { const promise = new Promise((resolve) => { resolve(42); }); - const resolved = await promise; - if (resolved == 42) { - return 1; - } else { - return -1; // unreachable - } + return await promise; // 42 } - createAndAwaitRejectingPromise(): number { + awaitRejectingPromise() { const promise = new Promise((resolve, reject) => { reject(new Error("An error occurred")); }); @@ -22,11 +17,11 @@ class Async { } awaitResolvedPromise(): number { - const promise = Promise.resolve(50); - return await promise; // 50 + const promise = Promise.resolve(42); + return await promise; // 42 } - awaitRejectedPromise(): number { + awaitRejectedPromise() { const promise = Promise.reject(new Error("An error occurred")); await promise; // exception } diff --git a/usvm-ts/src/test/resources/samples/Call.ts b/usvm-ts/src/test/resources/samples/Call.ts index 218800ff1d..653387e3f2 100644 --- a/usvm-ts/src/test/resources/samples/Call.ts +++ b/usvm-ts/src/test/resources/samples/Call.ts @@ -164,6 +164,87 @@ class Call { let x: A = makeA(); return x.foo(); // 20 (!!!) from B::foo } + + callLambda(): number { + const f = () => 42; + return f(); + } + + callClosureCapturingLocal(): number { + const x = 42; + const f = () => x; + return f(); + } + + callClosureCapturingArguments(a: boolean, b: boolean): number { + const f = () => a && b; + const res = f(); + if (res) { + return 1; + } else { + return 2; + } + } + + callNestedLambda(): number { + const f = () => { + const g = () => 42; + return g(); + }; + return f(); + } + + callNestedClosureCapturingOuterLocal(): number { + const x = 42; + const f = () => { + const g = () => x; + return g(); + }; + return f(); + } + + callNestedClosureCapturingInnerLocal(): number { + const f = () => { + const x = 42; + const g = () => x; + return g(); + }; + return f(); + } + + callNestedClosureCapturingLocalAndArgument(a: boolean): number { + const b = true; + const f = () => { + const g = () => a && b; + return g(); + }; + const res = f(); + if (res) { + return 1; + } else { + return 2; + } + } + + callClosureCapturingMutableLocal(): number { + let x = 42; + const f = () => { + x += 100; + return x + 5; + }; + x = 20; + return f() + x; // (20+100+5) + (20+100) = 245 + } + + callClosureMutatingCapturedLocal(): number { + let x = 42; + const f= () => { + x += 100; + }; + x = 20; + f(); + return x; // 120 + } } class A {