From 5c820ddbdb2a683c22571a116288900c41628fee Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sat, 3 Oct 2026 06:09:54 +0300 Subject: [PATCH 1/9] [TS] Construct instances from runtime constructor values --- .../kotlin/org/usvm/machine/expr/ReadField.kt | 33 ++- .../org/usvm/machine/expr/TsExprResolver.kt | 42 ++++ .../org/usvm/samples/types/RuntimeNewTest.kt | 231 ++++++++++++++++++ .../resources/samples/types/RuntimeNew.ts | 113 +++++++++ 4 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt create mode 100644 usvm-ts/src/test/resources/samples/types/RuntimeNew.ts diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt index 5eb05dfa69..5247f41790 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt @@ -17,6 +17,7 @@ import org.jacodb.ets.model.EtsStringType import org.jacodb.ets.model.EtsType import org.jacodb.ets.model.EtsUnclearRefType import org.jacodb.ets.model.EtsUndefinedType +import org.jacodb.ets.model.EtsUnionType import org.usvm.UBoolExpr import org.usvm.UConcreteHeapRef import org.usvm.UExpr @@ -25,6 +26,7 @@ import org.usvm.USort import org.usvm.USymbolicHeapRef import org.usvm.api.evalTypeEquals import org.usvm.api.makeSymbolicRefUntyped +import org.usvm.api.typeStreamOf import org.usvm.isAllocatedConcreteHeapRef import org.usvm.isFalse import org.usvm.isTrue @@ -39,6 +41,7 @@ import org.usvm.machine.types.iteWriteIntoFakeObject import org.usvm.machine.types.mkFakeValue import org.usvm.machine.types.requiresConstructorWitness import org.usvm.machine.types.toAuxiliaryType +import org.usvm.types.singleOrNull import org.usvm.util.EtsHierarchy import org.usvm.util.TsResolutionResult import org.usvm.util.arrayStorageType @@ -136,7 +139,14 @@ private fun TsContext.resolveDeclaredField( val resolvedField = resolveEtsField(local, field, hierarchy) val writtenSort = scope.calcOnState { writtenObjectLiteralFieldSorts[instance to field.name] } val declaredLiteralSort = declaredLiteralField?.let { typeToSort(it.type) } - val sort = writtenSort ?: declaredLiteralSort ?: resolvedFieldSort(scope, instance, field, resolvedField) + val sort = writtenSort ?: declaredLiteralSort ?: resolvedFieldSort( + scope = scope, + instanceLocal = local, + instance = instance, + field = field, + resolved = resolvedField, + hierarchy = hierarchy, + ) val declaredType = if (objectClass != null) { declaredLiteralField?.type } else { @@ -165,12 +175,31 @@ private fun TsContext.resolveDeclaredField( private fun TsContext.resolvedFieldSort( scope: TsStepScope, + instanceLocal: EtsLocal?, instance: UHeapRef, field: EtsFieldSignature, resolved: TsResolutionResult, + hierarchy: EtsHierarchy, ): USort = when (resolved) { is TsResolutionResult.Unique -> typeToSort(resolved.property.type) - is TsResolutionResult.Ambiguous -> unresolvedSort + is TsResolutionResult.Ambiguous -> { + // A dynamic constructor retains a union result type after allocating a concrete class. + val hasConcreteUnionReceiver = instanceLocal?.type is EtsUnionType && isAllocatedConcreteHeapRef(instance) + val runtimeType = if (hasConcreteUnionReceiver) { + scope.calcOnState { memory.typeStreamOf(instance).singleOrNull() } + } else { + null + } + val runtimeClass = (runtimeType as? EtsClassType)?.let { type -> + scene.projectAndSdkClasses.singleOrNull { it.signature == type.signature } + } + val runtimeFields = runtimeClass?.let { clazz -> + val ancestors = hierarchy.getAncestors(clazz) + resolved.properties.filter { it.declaringClass in ancestors } + } + + runtimeFields?.singleOrNull()?.let { typeToSort(it.type) } ?: unresolvedSort + } TsResolutionResult.Empty -> { if (field.name !in listOf("i", "LogLevel")) { logger.warn { "Field $field not found, creating fake field" } 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 894acb3c6a..572344a767 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 @@ -133,6 +133,7 @@ import org.usvm.machine.state.newStmt import org.usvm.machine.types.EtsNominalType import org.usvm.machine.types.iteWriteIntoFakeObject import org.usvm.sizeSort +import org.usvm.types.TypesResult import org.usvm.types.singleOrNull import org.usvm.util.EtsHierarchy import org.usvm.util.SymbolResolutionResult @@ -1479,6 +1480,8 @@ class TsExprResolver( // region OTHER override fun visit(expr: EtsNewExpr): UExpr? = with(ctx) { + expr.constructorValue?.let { return@with constructRuntimeClass(it) } + // Try to resolve the concrete type if possible. // Otherwise, create an object with UnclearRefType val resolvedType = if (expr.type.isResolved()) { @@ -1504,6 +1507,45 @@ class TsExprResolver( scope.calcOnState { memory.allocConcrete(resolvedType) } } + private fun constructRuntimeClass(constructorValue: EtsValue): UExpr? = with(ctx) { + val constructor = resolve(constructorValue) ?: return@with null + if (constructor.sort == fp64Sort || constructor.sort == boolSort) return@with throwTypeError() + if (constructor == mkTsNullValue() || constructor == mkUndefinedValue()) return@with throwTypeError() + + if (constructor.sort != addressSort || constructor.isFakeObject()) { + throw UnsupportedOperationException("Unresolved new constructor callability is not modeled") + } + + val constructorRef = constructor as? UConcreteHeapRef + ?: throw UnsupportedOperationException("Symbolic new constructor identity is not modeled") + if (getStringConstantValue(constructorRef) != null) return@with throwTypeError() + + val signature = classConstructorSignature(constructorRef) + ?: return@with resolveNonConstructorNew(constructorRef) + val clazz = scene.projectClasses.singleOrNull { it.signature == signature } + ?: throw UnsupportedOperationException("Unknown new project class: $signature") + + scope.calcOnState { memory.allocConcrete(clazz.type) } + } + + private fun resolveNonConstructorNew(ref: UConcreteHeapRef): Nothing? = with(ctx) { + val objectTypes = if (isAllocatedConcreteHeapRef(ref)) { + scope.calcOnState { memory.typeStreamOf(ref).take(n = 2) } + } else { + null + } + val objectType = (objectTypes as? TypesResult.SuccessfulTypesResult)?.types?.singleOrNull() + val objectClass = (objectType as? EtsClassType)?.let { type -> + scene.projectClasses.singleOrNull { it.signature == type.signature } + } + val isOrdinaryInstance = objectClass?.category == EtsClassCategory.CLASS || + objectClass?.category == EtsClassCategory.OBJECT + val isFunction = scope.calcOnState { associatedFunction[ref] != null } + if (isOrdinaryInstance && !isFunction) return@with throwTypeError() + + throw UnsupportedOperationException("Unknown new constructor value: $ref") + } + override fun visit(expr: EtsNewArrayExpr): UExpr? = with(ctx) { val arrayType = expr.type diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt new file mode 100644 index 0000000000..0126695d23 --- /dev/null +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -0,0 +1,231 @@ +package org.usvm.samples.types + +import org.jacodb.ets.model.EtsScene +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.usvm.StateCollectionStrategy +import org.usvm.UMachineOptions +import org.usvm.api.TsTestValue +import org.usvm.machine.TsAnalysisStopReason +import org.usvm.machine.TsMachine +import org.usvm.machine.TsOptions +import org.usvm.machine.state.TsMethodResult +import org.usvm.util.TsMethodTestRunner +import org.usvm.util.TsTestResolver +import org.usvm.util.getResourcePath +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class RuntimeNewTest : TsMethodTestRunner() { + @TempDir + lateinit var directory: Path + + private val tsPath = "/samples/types/RuntimeNew.ts" + + override val scene: EtsScene = loadScene(tsPath) + + @Test + fun `conditional constructor call selects class and executes its constructor`() { + for (methodName in listOf("dynamicCall", "inlineConditional", "localAlias")) { + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason, methodName) + assertTrue(outcome.unsupportedPaths.isEmpty(), "$methodName: ${outcome.unsupportedPaths}") + val cases = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + val input = assertIs(test.before.parameters.single()).value + val actual = assertIs(test.returnValue).number + assertEquals(if (input) 1.0 else 0.0, actual, "$methodName($input)") + input to actual + } + assertEquals(setOf(false, true), cases.map { it.first }.toSet(), methodName) + + replay(cases.map { (input, actual) -> + "if (new RuntimeNew().$methodName($input) !== ${actual.toInt()}) " + + "throw Error('$methodName($input)');" + }) + } + } + + @Test + fun `constructor identity is captured before an argument changes its binding`() { + assertSingleNumberResult(methodName = "sideEffectingArgument", expected = 1.0) + + replay(listOf( + "if (new RuntimeNew().sideEffectingArgument() !== 1) throw Error('constructor snapshot');" + )) + } + + @Test + fun `field read uses the selected class when a constructor union has common property names`() { + val numberMethod = getMethod(methodName = "valueFromEither", className = "RuntimeNew") + val numberOutcome = analyze("valueFromEither") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, numberOutcome.stopReason) + assertTrue(numberOutcome.unsupportedPaths.isEmpty(), "${numberOutcome.unsupportedPaths}") + val numberCases = numberOutcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(numberMethod, state) + val input = assertIs(test.before.parameters.single()).value + assertEquals(7.0, assertIs(test.returnValue).number) + input + } + assertEquals(setOf(false, true), numberCases.toSet()) + + val sortMethod = getMethod(methodName = "fieldSortByRuntimeClass", className = "RuntimeNew") + val sortOutcome = analyze("fieldSortByRuntimeClass") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, sortOutcome.stopReason) + assertTrue(sortOutcome.unsupportedPaths.isEmpty(), "${sortOutcome.unsupportedPaths}") + val sortCases = sortOutcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(sortMethod, state) + val input = assertIs(test.before.parameters.single()).value + assertTrue(assertIs(test.returnValue).value) + input + } + assertEquals(setOf(false, true), sortCases.toSet()) + + replay(numberCases.map { input -> + "if (new RuntimeNew().valueFromEither($input) !== 7) throw Error('field number $input');" + } + sortCases.map { input -> + "if (new RuntimeNew().fieldSortByRuntimeClass($input) !== true) throw Error('field sort $input');" + }) + } + + @Test + fun `selected constructor executes exactly once`() { + val method = getMethod(methodName = "constructorCalledOnce", className = "RuntimeNew") + val outcome = analyze("constructorCalledOnce") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.isEmpty(), "${outcome.unsupportedPaths}") + val cases = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + val input = assertIs(test.before.parameters.single()).value + assertTrue(assertIs(test.returnValue).value) + input + } + assertEquals(setOf(false, true), cases.toSet()) + + replay(cases.map { input -> + "if (new RuntimeNew().constructorCalledOnce($input) !== true) throw Error('ctor count $input');" + }) + } + + @Test + fun `direct construction still executes the constructor`() { + assertSingleNumberResult(methodName = "direct", expected = 1.0) + + replay(listOf( + "if (new RuntimeNew().direct() !== 1) throw Error('direct construction');" + )) + } + + @Test + fun `known non constructor throws TypeError`() { + val outcome = analyze("nonConstructable") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.isEmpty(), "${outcome.unsupportedPaths}") + assertTrue(outcome.states.isNotEmpty()) + outcome.states.forEach { state -> + val exception = assertIs(state.methodResult) + assertEquals("TypeError", exception.type.typeName) + } + + replay(listOf( + "let threwTypeError = false; try { new RuntimeNew().nonConstructable(); } " + + "catch (error) { threwTypeError = error instanceof TypeError; } " + + "if (!threwTypeError) throw Error('non constructor');" + )) + } + + @Test + fun `unmodeled constructor identity is explicitly unsupported`() { + val method = getMethod(methodName = "symbolicConstructor", className = "RuntimeNew") + val outcome = TsMachine(scene, options = machineOptions, tsOptions = TsOptions()).use { machine -> + machine.analyzeWithOutcome(methods = listOf(method)) + } + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.states.isEmpty()) + assertTrue(outcome.unsupportedPaths.any { "Constructor-typed parameter" in it }, + "${outcome.unsupportedPaths}") + + val unknownOutcome = analyze("unknownConstructor") + assertEquals(TsAnalysisStopReason.EXHAUSTED, unknownOutcome.stopReason) + assertTrue(unknownOutcome.states.isEmpty()) + assertTrue(unknownOutcome.unsupportedPaths.any { "new constructor" in it }, + "${unknownOutcome.unsupportedPaths}") + } + + @Test + fun `known object instance throws TypeError instead of allocating`() { + val outcome = analyze("nonConstructableObject") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.isEmpty(), "${outcome.unsupportedPaths}") + assertTrue(outcome.states.isNotEmpty()) + outcome.states.forEach { state -> + val exception = assertIs(state.methodResult) + assertEquals("TypeError", exception.type.typeName) + } + + replay(listOf( + "let threwTypeError = false; try { new RuntimeNew().nonConstructableObject(); } " + + "catch (error) { threwTypeError = error instanceof TypeError; } " + + "if (!threwTypeError) throw Error('object constructor');" + )) + } + + private fun assertSingleNumberResult(methodName: String, expected: Double) { + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.isEmpty(), "$methodName: ${outcome.unsupportedPaths}") + assertTrue(outcome.states.isNotEmpty()) + outcome.states.forEach { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + assertEquals(expected, assertIs(test.returnValue).number) + } + } + + private fun analyze(methodName: String) = TsMachine(scene, options = machineOptions, tsOptions = TsOptions()).use { machine -> + machine.analyzeWithOutcome(methods = listOf(getMethod(methodName = methodName, className = "RuntimeNew"))) + } + + private fun replay(assertions: List) { + val script = directory.resolve("runtime-new.ts") + val output = directory.resolve("runtime-new.out") + script.writeText(buildString { + appendLine(getResourcePath(tsPath).readText()) + assertions.forEach(::appendLine) + }) + + val process = ProcessBuilder("node", "--experimental-strip-types", script.toString()) + .redirectErrorStream(true) + .redirectOutput(output.toFile()) + .start() + + assertTrue(process.waitFor(30, TimeUnit.SECONDS), "Node replay timed out") + assertEquals(0, process.exitValue(), output.readText()) + } + + private val machineOptions = UMachineOptions( + stateCollectionStrategy = StateCollectionStrategy.ALL, + stopOnCoverage = 0, + timeout = 30.seconds, + ) +} diff --git a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts new file mode 100644 index 0000000000..67fe9f6daf --- /dev/null +++ b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts @@ -0,0 +1,113 @@ +class NewA { + value: number; + + constructor(value: number) { + this.value = value; + } +} + +class NewB { + value: number; + + constructor(value: number) { + this.value = value; + } +} + +class NewText { + value: string; + + constructor(_value: number) { + this.value = "seven"; + } +} + +class ConstructorCounter { + count: number; + + constructor() { + this.count = 0; + } +} + +class CountA { + constructor(counter: ConstructorCounter) { + counter.count = counter.count + 1; + } +} + +class CountB { + constructor(counter: ConstructorCounter) { + counter.count = counter.count + 1; + } +} + +class RuntimeNew { + choose(useA: boolean): typeof NewA | typeof NewB { + return useA ? NewA : NewB; + } + + dynamicCall(useA: boolean): number { + const value = new (this.choose(useA))(7); + return value instanceof NewA && value.value === 7 ? 1 : 0; + } + + inlineConditional(useA: boolean): number { + const value = new (useA ? NewA : NewB)(7); + return value instanceof NewA && value.value === 7 ? 1 : 0; + } + + localAlias(useA: boolean): number { + const selected = this.choose(useA); + const alias = selected; + const value = new alias(7); + return value instanceof NewA && value.value === 7 ? 1 : 0; + } + + valueFromEither(useA: boolean): number { + const value = new (this.choose(useA))(7); + return value.value; + } + + fieldSortByRuntimeClass(useNumber: boolean): boolean { + const value = new (useNumber ? NewA : NewText)(7); + return useNumber ? value.value === 7 : value.value === "seven"; + } + + constructorCalledOnce(useA: boolean): boolean { + const counter = new ConstructorCounter(); + const value = new (useA ? CountA : CountB)(counter); + return counter.count === 1 && (value instanceof CountA) === useA; + } + + sideEffectingArgument(): number { + let selected: typeof NewA | typeof NewB = NewA; + const value = new selected((selected = NewB, 7)); + return value instanceof NewA && (value as NewA).value === 7 && selected === NewB ? 1 : 0; + } + + direct(): number { + const value = new NewA(7); + return value instanceof NewA && value.value === 7 ? 1 : 0; + } + + nonConstructable(): number { + const selected: any = 42; + const value = new selected(); + return value instanceof NewA ? 1 : 0; + } + + nonConstructableObject(): number { + const selected: any = new NewA(7); + const value = new selected(); + return value instanceof NewA ? 1 : 0; + } + + symbolicConstructor(selected: typeof NewA): number { + return new selected(7).value; + } + + unknownConstructor(selected: any): number { + return new selected(7).value; + } +} From 21235e98ea237598ac4279cde3d1c8dae69fb9b6 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sat, 3 Oct 2026 06:47:56 +0300 Subject: [PATCH 2/9] [TS] Keep runtime constructor field writes consistent with reads --- .../kotlin/org/usvm/machine/expr/ReadField.kt | 28 ++---- .../org/usvm/machine/expr/RuntimeFieldSort.kt | 37 +++++++ .../org/usvm/machine/expr/WriteField.kt | 44 +++++++-- .../org/usvm/samples/types/RuntimeNewTest.kt | 99 +++++++++++++++++++ .../resources/samples/types/RuntimeNew.ts | 33 +++++++ 5 files changed, 211 insertions(+), 30 deletions(-) create mode 100644 usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt index 5247f41790..263a2afc93 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt @@ -17,7 +17,6 @@ import org.jacodb.ets.model.EtsStringType import org.jacodb.ets.model.EtsType import org.jacodb.ets.model.EtsUnclearRefType import org.jacodb.ets.model.EtsUndefinedType -import org.jacodb.ets.model.EtsUnionType import org.usvm.UBoolExpr import org.usvm.UConcreteHeapRef import org.usvm.UExpr @@ -26,7 +25,6 @@ import org.usvm.USort import org.usvm.USymbolicHeapRef import org.usvm.api.evalTypeEquals import org.usvm.api.makeSymbolicRefUntyped -import org.usvm.api.typeStreamOf import org.usvm.isAllocatedConcreteHeapRef import org.usvm.isFalse import org.usvm.isTrue @@ -41,7 +39,6 @@ import org.usvm.machine.types.iteWriteIntoFakeObject import org.usvm.machine.types.mkFakeValue import org.usvm.machine.types.requiresConstructorWitness import org.usvm.machine.types.toAuxiliaryType -import org.usvm.types.singleOrNull import org.usvm.util.EtsHierarchy import org.usvm.util.TsResolutionResult import org.usvm.util.arrayStorageType @@ -182,24 +179,13 @@ private fun TsContext.resolvedFieldSort( hierarchy: EtsHierarchy, ): USort = when (resolved) { is TsResolutionResult.Unique -> typeToSort(resolved.property.type) - is TsResolutionResult.Ambiguous -> { - // A dynamic constructor retains a union result type after allocating a concrete class. - val hasConcreteUnionReceiver = instanceLocal?.type is EtsUnionType && isAllocatedConcreteHeapRef(instance) - val runtimeType = if (hasConcreteUnionReceiver) { - scope.calcOnState { memory.typeStreamOf(instance).singleOrNull() } - } else { - null - } - val runtimeClass = (runtimeType as? EtsClassType)?.let { type -> - scene.projectAndSdkClasses.singleOrNull { it.signature == type.signature } - } - val runtimeFields = runtimeClass?.let { clazz -> - val ancestors = hierarchy.getAncestors(clazz) - resolved.properties.filter { it.declaringClass in ancestors } - } - - runtimeFields?.singleOrNull()?.let { typeToSort(it.type) } ?: unresolvedSort - } + is TsResolutionResult.Ambiguous -> resolveAmbiguousFieldSort( + scope = scope, + instanceLocal = instanceLocal, + instance = instance, + fields = resolved.properties, + hierarchy = hierarchy, + ) TsResolutionResult.Empty -> { if (field.name !in listOf("i", "LogLevel")) { logger.warn { "Field $field not found, creating fake field" } diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt new file mode 100644 index 0000000000..f2815465c7 --- /dev/null +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt @@ -0,0 +1,37 @@ +package org.usvm.machine.expr + +import org.jacodb.ets.model.EtsClassType +import org.jacodb.ets.model.EtsField +import org.jacodb.ets.model.EtsLocal +import org.jacodb.ets.model.EtsUnionType +import org.usvm.UHeapRef +import org.usvm.USort +import org.usvm.api.typeStreamOf +import org.usvm.isAllocatedConcreteHeapRef +import org.usvm.machine.TsContext +import org.usvm.machine.interpreter.TsStepScope +import org.usvm.types.singleOrNull +import org.usvm.util.EtsHierarchy + +internal fun TsContext.resolveAmbiguousFieldSort( + scope: TsStepScope, + instanceLocal: EtsLocal?, + instance: UHeapRef, + fields: List, + hierarchy: EtsHierarchy, +): USort { + // A dynamic constructor leaves the local union-typed after allocating one concrete class. + // Keep the existing representation for other ambiguous and any-typed fields. + if (instanceLocal?.type !is EtsUnionType || !isAllocatedConcreteHeapRef(instance)) { + return unresolvedSort + } + + val runtimeType = scope.calcOnState { memory.typeStreamOf(instance).singleOrNull() } + val runtimeClass = (runtimeType as? EtsClassType)?.let { type -> + scene.projectAndSdkClasses.singleOrNull { it.signature == type.signature } + } ?: return unresolvedSort + + val ancestors = hierarchy.getAncestors(runtimeClass) + val runtimeField = fields.singleOrNull { it.declaringClass in ancestors } ?: return unresolvedSort + return typeToSort(runtimeField.type) +} diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt index 666ea07b15..2ca9755fb5 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt @@ -3,12 +3,10 @@ package org.usvm.machine.expr import io.ksmt.utils.asExpr import mu.KotlinLogging import org.jacodb.ets.model.EtsArrayType -import org.jacodb.ets.model.EtsBooleanType import org.jacodb.ets.model.EtsClosureFieldRef import org.jacodb.ets.model.EtsFieldSignature import org.jacodb.ets.model.EtsInstanceFieldRef import org.jacodb.ets.model.EtsLocal -import org.jacodb.ets.model.EtsNumberType import org.jacodb.ets.model.EtsStaticFieldRef import org.jacodb.ets.model.EtsType import org.usvm.UExpr @@ -205,10 +203,37 @@ fun TsContext.assignToInstanceField( else -> when (val etsField = resolveEtsField(instanceLocal, field, hierarchy)) { is TsResolutionResult.Empty -> unresolvedSort is TsResolutionResult.Unique -> typeToSort(etsField.property.type) - is TsResolutionResult.Ambiguous -> unresolvedSort + is TsResolutionResult.Ambiguous -> resolveAmbiguousFieldSort( + scope = scope, + instanceLocal = instanceLocal, + instance = unwrappedInstance, + fields = etsField.properties, + hierarchy = hierarchy, + ) } } + if (sort !is TsUnresolvedSort && expr.isFakeObject()) { + val fakeType = scope.calcOnState { expr.getFakeType(scope) } + val compatibleType = when (sort) { + boolSort -> fakeType.boolTypeExpr + fp64Sort -> fakeType.fpTypeExpr + addressSort -> fakeType.refTypeExpr + else -> error("Unsupported field sort: $sort") + } + + scope.fork( + compatibleType, + blockOnFalseState = { + terminateAsUnsupported(reason = "Assignment changes runtime sort of field '${field.name}'") + }, + ) ?: return + } + + if (sort !is TsUnresolvedSort && expr.sort != sort && !expr.isFakeObject()) { + throw UnsupportedOperationException("Assignment changes runtime sort of field '${field.name}'") + } + // If the field type is unknown, we create a fake object for the expr and assign it. // Otherwise, assign expr directly. scope.doWithState { @@ -221,26 +246,27 @@ fun TsContext.assignToInstanceField( val lValue = mkFieldLValue(sort, unwrappedInstance, field) if (lValue.sort != expr.sort) { if (expr.isFakeObject()) { - val lhvType = instanceLocal.type - val value = when (lhvType) { - is EtsBooleanType -> { + val value = when (sort) { + boolSort -> { pathConstraints += expr.getFakeType(scope).boolTypeExpr expr.extractBool(scope) } - is EtsNumberType -> { + fp64Sort -> { pathConstraints += expr.getFakeType(scope).fpTypeExpr expr.extractFp(scope) } - else -> { + addressSort -> { pathConstraints += expr.getFakeType(scope).refTypeExpr expr.extractRef(scope) } + + else -> error("Unsupported field sort: $sort") } memory.write(lValue, value.asExpr(lValue.sort), guard = trueExpr) } else { - TODO("Support enums fields") + error("Incompatible field value should have been marked unsupported") } } else { memory.write(lValue, expr.asExpr(lValue.sort), guard = trueExpr) diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index 0126695d23..120936d080 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -101,6 +101,105 @@ class RuntimeNewTest : TsMethodTestRunner() { }) } + @Test + fun `field writes and aliases use the selected class`() { + for (methodName in listOf("writeToEither", "writeThroughAlias")) { + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason, methodName) + assertTrue(outcome.unsupportedPaths.isEmpty(), "$methodName: ${outcome.unsupportedPaths}") + val cases = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + val input = assertIs(test.before.parameters.single()).value + val actual = assertIs(test.returnValue).number + val expected = if (methodName == "writeToEither") 5.0 else 11.0 + assertEquals(expected, actual, "$methodName($input)") + input to actual + } + assertEquals(setOf(false, true), cases.map { it.first }.toSet(), methodName) + + replay(cases.map { (input, actual) -> + "if (new RuntimeNew().$methodName($input) !== ${actual.toInt()}) " + + "throw Error('$methodName($input)');" + }) + } + } + + @Test + fun `field writes respect the selected runtime field sort`() { + val methodName = "writeFieldWithDifferentRuntimeSort" + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.isEmpty(), "${outcome.unsupportedPaths}") + val cases = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + val input = assertIs(test.before.parameters.single()).value + assertTrue(assertIs(test.returnValue).value) + input + } + assertEquals(setOf(false, true), cases.toSet()) + + replay(cases.map { input -> + "if (new RuntimeNew().$methodName($input) !== true) throw Error('$methodName($input)');" + }) + } + + @Test + fun `runtime field sort change is reported as unsupported`() { + val methodName = "writeIncompatibleRuntimeField" + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.any { "Assignment changes runtime sort" in it }, + "${outcome.unsupportedPaths}") + val cases = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + val input = assertIs(test.before.parameters.single()).value + assertTrue(assertIs(test.returnValue).value) + input + } + assertEquals(setOf(false), cases.toSet()) + + replay(listOf( + "if (new RuntimeNew().$methodName(false) !== true) throw Error('supported field sort');", + "if (new RuntimeNew().$methodName(true) !== true) throw Error('unsupported field sort');", + )) + } + + @Test + fun `incompatible alternatives of a union field value are reported as unsupported`() { + val methodName = "writePossiblyIncompatibleRuntimeField" + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.any { "Assignment changes runtime sort" in it }, + "${outcome.unsupportedPaths}") + val cases = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + val input = test.before.parameters.map { assertIs(it).value } + assertEquals(2, input.size) + assertTrue(assertIs(test.returnValue).value) + input + } + assertEquals(setOf(listOf(true, false), listOf(false, true)), cases.toSet()) + + replay(listOf( + "if (new RuntimeNew().$methodName(true, false) !== true) throw Error('numeric field');", + "if (new RuntimeNew().$methodName(false, true) !== true) throw Error('string field');", + "if (new RuntimeNew().$methodName(true, true) !== true) throw Error('numeric type change');", + "if (new RuntimeNew().$methodName(false, false) !== true) throw Error('string type change');", + )) + } + @Test fun `selected constructor executes exactly once`() { val method = getMethod(methodName = "constructorCalledOnce", className = "RuntimeNew") diff --git a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts index 67fe9f6daf..1b8bddf01c 100644 --- a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts +++ b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts @@ -1,5 +1,6 @@ class NewA { value: number; + onlyA: number = 1; constructor(value: number) { this.value = value; @@ -8,6 +9,7 @@ class NewA { class NewB { value: number; + onlyB: number = 2; constructor(value: number) { this.value = value; @@ -74,6 +76,37 @@ class RuntimeNew { return useNumber ? value.value === 7 : value.value === "seven"; } + writeToEither(useA: boolean): number { + const value = new (useA ? NewA : NewB)(7); + value.value = 5; + return value.value; + } + + writeThroughAlias(useA: boolean): number { + const value = new (useA ? NewA : NewB)(7); + const alias = value; + alias.value = 11; + return value.value; + } + + writeFieldWithDifferentRuntimeSort(useNumber: boolean): boolean { + const value = new (useNumber ? NewA : NewText)(7); + value.value = useNumber ? 9 : "nine"; + return useNumber ? value.value === 9 : value.value === "nine"; + } + + writeIncompatibleRuntimeField(useNumber: boolean): boolean { + const value = new (useNumber ? NewA : NewText)(7); + value.value = "changed"; + return value.value === "changed"; + } + + writePossiblyIncompatibleRuntimeField(useNumber: boolean, writeString: boolean): boolean { + const value = new (useNumber ? NewA : NewText)(7); + value.value = writeString ? "changed" : 9; + return writeString ? value.value === "changed" : value.value === 9; + } + constructorCalledOnce(useA: boolean): boolean { const counter = new ConstructorCounter(); const value = new (useA ? CountA : CountB)(counter); From 76adc943b07e2bdead419b9a7377d1b4dc0b666a Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sat, 3 Oct 2026 07:09:34 +0300 Subject: [PATCH 3/9] [TS] Remove redundant field-write checks --- .../org/usvm/machine/expr/WriteField.kt | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt index 2ca9755fb5..4c45b66f38 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt @@ -245,29 +245,14 @@ fun TsContext.assignToInstanceField( } else { val lValue = mkFieldLValue(sort, unwrappedInstance, field) if (lValue.sort != expr.sort) { - if (expr.isFakeObject()) { - val value = when (sort) { - boolSort -> { - pathConstraints += expr.getFakeType(scope).boolTypeExpr - expr.extractBool(scope) - } - - fp64Sort -> { - pathConstraints += expr.getFakeType(scope).fpTypeExpr - expr.extractFp(scope) - } - - addressSort -> { - pathConstraints += expr.getFakeType(scope).refTypeExpr - expr.extractRef(scope) - } - - else -> error("Unsupported field sort: $sort") - } - memory.write(lValue, value.asExpr(lValue.sort), guard = trueExpr) - } else { - error("Incompatible field value should have been marked unsupported") + check(expr.isFakeObject()) + val value = when (sort) { + boolSort -> expr.extractBool(scope) + fp64Sort -> expr.extractFp(scope) + addressSort -> expr.extractRef(scope) + else -> error("Unsupported field sort: $sort") } + memory.write(lValue, value.asExpr(lValue.sort), guard = trueExpr) } else { memory.write(lValue, expr.asExpr(lValue.sort), guard = trueExpr) } From 8d7887c81abd0ced90b285eafd52d7479d481fcc Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sat, 3 Oct 2026 08:52:43 +0300 Subject: [PATCH 4/9] [TS] Reuse shared Node replay for runtime new tests --- .../org/usvm/samples/types/RuntimeNewTest.kt | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index 120936d080..cecc9014e4 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -12,11 +12,10 @@ import org.usvm.machine.TsOptions import org.usvm.machine.state.TsMethodResult import org.usvm.util.TsMethodTestRunner import org.usvm.util.TsTestResolver +import org.usvm.util.assertNodeReplay import org.usvm.util.getResourcePath import java.nio.file.Path -import java.util.concurrent.TimeUnit import kotlin.io.path.readText -import kotlin.io.path.writeText import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertTrue @@ -306,20 +305,17 @@ class RuntimeNewTest : TsMethodTestRunner() { } private fun replay(assertions: List) { - val script = directory.resolve("runtime-new.ts") - val output = directory.resolve("runtime-new.out") - script.writeText(buildString { + val source = buildString { appendLine(getResourcePath(tsPath).readText()) assertions.forEach(::appendLine) - }) - - val process = ProcessBuilder("node", "--experimental-strip-types", script.toString()) - .redirectErrorStream(true) - .redirectOutput(output.toFile()) - .start() + } - assertTrue(process.waitFor(30, TimeUnit.SECONDS), "Node replay timed out") - assertEquals(0, process.exitValue(), output.readText()) + assertNodeReplay( + source = source, + directory = directory, + name = "runtime-new", + timeoutMessage = "Node replay timed out", + ) } private val machineOptions = UMachineOptions( From 37c1ba9e536f75aa6865370a41ebbad9afce5424 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sun, 4 Oct 2026 01:23:26 +0300 Subject: [PATCH 5/9] [TS] Check runtime constructor results through discoverProperties --- .../org/usvm/samples/types/RuntimeNewTest.kt | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index cecc9014e4..b0ac85bb8f 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -33,6 +33,14 @@ class RuntimeNewTest : TsMethodTestRunner() { fun `conditional constructor call selects class and executes its constructor`() { for (methodName in listOf("dynamicCall", "inlineConditional", "localAlias")) { val method = getMethod(methodName = methodName, className = "RuntimeNew") + + discoverProperties( + method = method, + { input, result -> input.value && result.number == 1.0 }, + { input, result -> !input.value && result.number == 0.0 }, + invariants = arrayOf({ input, result -> result.number == (if (input.value) 1.0 else 0.0) }), + ) + val outcome = analyze(methodName) assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason, methodName) @@ -66,6 +74,16 @@ class RuntimeNewTest : TsMethodTestRunner() { @Test fun `field read uses the selected class when a constructor union has common property names`() { val numberMethod = getMethod(methodName = "valueFromEither", className = "RuntimeNew") + + withOptions(options = options.copy(stateCollectionStrategy = StateCollectionStrategy.ALL, stopOnCoverage = 0)) { + discoverProperties( + method = numberMethod, + { input, result -> input.value && result.number == 7.0 }, + { input, result -> !input.value && result.number == 7.0 }, + invariants = arrayOf({ _, result -> result.number == 7.0 }), + ) + } + val numberOutcome = analyze("valueFromEither") assertEquals(TsAnalysisStopReason.EXHAUSTED, numberOutcome.stopReason) @@ -80,6 +98,14 @@ class RuntimeNewTest : TsMethodTestRunner() { assertEquals(setOf(false, true), numberCases.toSet()) val sortMethod = getMethod(methodName = "fieldSortByRuntimeClass", className = "RuntimeNew") + + discoverProperties( + method = sortMethod, + { input, result -> input.value && result.value }, + { input, result -> !input.value && result.value }, + invariants = arrayOf({ _, result -> result.value }), + ) + val sortOutcome = analyze("fieldSortByRuntimeClass") assertEquals(TsAnalysisStopReason.EXHAUSTED, sortOutcome.stopReason) @@ -104,6 +130,15 @@ class RuntimeNewTest : TsMethodTestRunner() { fun `field writes and aliases use the selected class`() { for (methodName in listOf("writeToEither", "writeThroughAlias")) { val method = getMethod(methodName = methodName, className = "RuntimeNew") + val expected = if (methodName == "writeToEither") 5.0 else 11.0 + + discoverProperties( + method = method, + { input, result -> input.value && result.number == expected }, + { input, result -> !input.value && result.number == expected }, + invariants = arrayOf({ _, result -> result.number == expected }), + ) + val outcome = analyze(methodName) assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason, methodName) @@ -113,7 +148,6 @@ class RuntimeNewTest : TsMethodTestRunner() { val test = TsTestResolver().resolve(method, state) val input = assertIs(test.before.parameters.single()).value val actual = assertIs(test.returnValue).number - val expected = if (methodName == "writeToEither") 5.0 else 11.0 assertEquals(expected, actual, "$methodName($input)") input to actual } @@ -130,6 +164,14 @@ class RuntimeNewTest : TsMethodTestRunner() { fun `field writes respect the selected runtime field sort`() { val methodName = "writeFieldWithDifferentRuntimeSort" val method = getMethod(methodName = methodName, className = "RuntimeNew") + + discoverProperties( + method = method, + { input, result -> input.value && result.value }, + { input, result -> !input.value && result.value }, + invariants = arrayOf({ _, result -> result.value }), + ) + val outcome = analyze(methodName) assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) @@ -202,6 +244,14 @@ class RuntimeNewTest : TsMethodTestRunner() { @Test fun `selected constructor executes exactly once`() { val method = getMethod(methodName = "constructorCalledOnce", className = "RuntimeNew") + + discoverProperties( + method = method, + { input, result -> input.value && result.value }, + { input, result -> !input.value && result.value }, + invariants = arrayOf({ _, result -> result.value }), + ) + val outcome = analyze("constructorCalledOnce") assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) @@ -288,6 +338,13 @@ class RuntimeNewTest : TsMethodTestRunner() { private fun assertSingleNumberResult(methodName: String, expected: Double) { val method = getMethod(methodName = methodName, className = "RuntimeNew") + + discoverProperties( + method = method, + { result -> result.number == expected }, + invariants = arrayOf({ result -> result.number == expected }), + ) + val outcome = analyze(methodName) assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) From 5f82cf691deca4e4fa58fd9ffe4ebad8d4219ab9 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sun, 11 Oct 2026 20:34:34 +0300 Subject: [PATCH 6/9] [TS] Throw TypeError for computed string constructors --- .../org/usvm/machine/expr/TsExprResolver.kt | 2 ++ .../org/usvm/samples/types/RuntimeNewTest.kt | 29 +++++++++++++++++++ .../resources/samples/types/RuntimeNew.ts | 22 ++++++++++++++ 3 files changed, 53 insertions(+) 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 572344a767..06bbe927e0 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 @@ -1535,6 +1535,8 @@ class TsExprResolver( null } val objectType = (objectTypes as? TypesResult.SuccessfulTypesResult)?.types?.singleOrNull() + if (objectType == EtsStringType) return@with throwTypeError() + val objectClass = (objectType as? EtsClassType)?.let { type -> scene.projectClasses.singleOrNull { it.signature == type.signature } } diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index b0ac85bb8f..98a58b5eea 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -298,6 +298,35 @@ class RuntimeNewTest : TsMethodTestRunner() { )) } + @Test + fun `non constructable values reach the catch block`() { + discoverProperties( + method = getMethod(methodName = "caughtNonConstructable", className = "RuntimeNew"), + { result -> result.number == 1.0 }, + invariants = arrayOf({ result -> result.number == 1.0 }), + ) + + val stringOutcome = analyze(methodName = "caughtNonConstructableString") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, stringOutcome.stopReason) + assertTrue(stringOutcome.unsupportedPaths.isEmpty(), "${stringOutcome.unsupportedPaths}") + + discoverProperties( + method = getMethod(methodName = "caughtNonConstructableString", className = "RuntimeNew"), + { input, result -> input.value.length == 1 && result.number == 1.0 }, + { input, result -> input.value.length != 1 && result.number == 0.0 }, + invariants = arrayOf({ input, result -> + val expected = if (input.value.length == 1) 1.0 else 0.0 + result.number == expected + }), + ) + + replay(listOf( + "if (new RuntimeNew().caughtNonConstructable() !== 1) throw Error('caught numeric TypeError');", + "if (new RuntimeNew().caughtNonConstructableString('a') !== 1) throw Error('caught string TypeError');" + )) + } + @Test fun `unmodeled constructor identity is explicitly unsupported`() { val method = getMethod(methodName = "symbolicConstructor", className = "RuntimeNew") diff --git a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts index 1b8bddf01c..9316a4d17e 100644 --- a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts +++ b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts @@ -130,6 +130,28 @@ class RuntimeNew { return value instanceof NewA ? 1 : 0; } + caughtNonConstructable(): number { + try { + const selected: any = 42; + new selected(); + return 0; + } catch { + return 1; + } + } + + caughtNonConstructableString(value: string): number { + if (value.length !== 1) return 0; + + try { + const selected: any = value + "b"; + new selected(); + return 0; + } catch { + return 1; + } + } + nonConstructableObject(): number { const selected: any = new NewA(7); const value = new selected(); From dcd04f8a84ed85969dc859368c24ba2a7224f425 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Mon, 12 Oct 2026 15:31:59 +0300 Subject: [PATCH 7/9] [TS] Preserve boxed field writes and share constructor validation --- .../kotlin/org/usvm/machine/expr/ReadField.kt | 3 - .../org/usvm/machine/expr/RuntimeFieldSort.kt | 9 +-- .../org/usvm/machine/expr/TsExprResolver.kt | 41 +--------- .../org/usvm/machine/expr/WriteArray.kt | 3 +- .../org/usvm/machine/expr/WriteField.kt | 20 ++--- .../org/usvm/samples/types/RuntimeNewTest.kt | 80 +++++++++++++++++++ .../resources/samples/types/RuntimeNew.ts | 67 ++++++++++++++++ 7 files changed, 165 insertions(+), 58 deletions(-) diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt index 263a2afc93..1bd686f5d5 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt @@ -138,7 +138,6 @@ private fun TsContext.resolveDeclaredField( val declaredLiteralSort = declaredLiteralField?.let { typeToSort(it.type) } val sort = writtenSort ?: declaredLiteralSort ?: resolvedFieldSort( scope = scope, - instanceLocal = local, instance = instance, field = field, resolved = resolvedField, @@ -172,7 +171,6 @@ private fun TsContext.resolveDeclaredField( private fun TsContext.resolvedFieldSort( scope: TsStepScope, - instanceLocal: EtsLocal?, instance: UHeapRef, field: EtsFieldSignature, resolved: TsResolutionResult, @@ -181,7 +179,6 @@ private fun TsContext.resolvedFieldSort( is TsResolutionResult.Unique -> typeToSort(resolved.property.type) is TsResolutionResult.Ambiguous -> resolveAmbiguousFieldSort( scope = scope, - instanceLocal = instanceLocal, instance = instance, fields = resolved.properties, hierarchy = hierarchy, diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt index f2815465c7..2915373740 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt @@ -2,8 +2,6 @@ package org.usvm.machine.expr import org.jacodb.ets.model.EtsClassType import org.jacodb.ets.model.EtsField -import org.jacodb.ets.model.EtsLocal -import org.jacodb.ets.model.EtsUnionType import org.usvm.UHeapRef import org.usvm.USort import org.usvm.api.typeStreamOf @@ -15,14 +13,13 @@ import org.usvm.util.EtsHierarchy internal fun TsContext.resolveAmbiguousFieldSort( scope: TsStepScope, - instanceLocal: EtsLocal?, instance: UHeapRef, fields: List, hierarchy: EtsHierarchy, ): USort { - // A dynamic constructor leaves the local union-typed after allocating one concrete class. - // Keep the existing representation for other ambiguous and any-typed fields. - if (instanceLocal?.type !is EtsUnionType || !isAllocatedConcreteHeapRef(instance)) { + // The allocated class determines storage even when a union or any-typed local + // (including a computed property access) leaves the frontend field ambiguous. + if (!isAllocatedConcreteHeapRef(instance)) { return unresolvedSort } 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 06bbe927e0..283441ffc6 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 @@ -133,7 +133,6 @@ import org.usvm.machine.state.newStmt import org.usvm.machine.types.EtsNominalType import org.usvm.machine.types.iteWriteIntoFakeObject import org.usvm.sizeSort -import org.usvm.types.TypesResult import org.usvm.types.singleOrNull import org.usvm.util.EtsHierarchy import org.usvm.util.SymbolResolutionResult @@ -1508,46 +1507,14 @@ class TsExprResolver( } private fun constructRuntimeClass(constructorValue: EtsValue): UExpr? = with(ctx) { - val constructor = resolve(constructorValue) ?: return@with null - if (constructor.sort == fp64Sort || constructor.sort == boolSort) return@with throwTypeError() - if (constructor == mkTsNullValue() || constructor == mkUndefinedValue()) return@with throwTypeError() - - if (constructor.sort != addressSort || constructor.isFakeObject()) { - throw UnsupportedOperationException("Unresolved new constructor callability is not modeled") - } - - val constructorRef = constructor as? UConcreteHeapRef - ?: throw UnsupportedOperationException("Symbolic new constructor identity is not modeled") - if (getStringConstantValue(constructorRef) != null) return@with throwTypeError() - - val signature = classConstructorSignature(constructorRef) - ?: return@with resolveNonConstructorNew(constructorRef) - val clazz = scene.projectClasses.singleOrNull { it.signature == signature } - ?: throw UnsupportedOperationException("Unknown new project class: $signature") + val constructorType = resolveRuntimeConstructor(constructorValue, operation = RuntimeConstructorOperation.NEW) + ?: return@with null + val clazz = scene.projectClasses.singleOrNull { it.signature == constructorType.signature } + ?: throw UnsupportedOperationException("Unknown new project class: ${constructorType.signature}") scope.calcOnState { memory.allocConcrete(clazz.type) } } - private fun resolveNonConstructorNew(ref: UConcreteHeapRef): Nothing? = with(ctx) { - val objectTypes = if (isAllocatedConcreteHeapRef(ref)) { - scope.calcOnState { memory.typeStreamOf(ref).take(n = 2) } - } else { - null - } - val objectType = (objectTypes as? TypesResult.SuccessfulTypesResult)?.types?.singleOrNull() - if (objectType == EtsStringType) return@with throwTypeError() - - val objectClass = (objectType as? EtsClassType)?.let { type -> - scene.projectClasses.singleOrNull { it.signature == type.signature } - } - val isOrdinaryInstance = objectClass?.category == EtsClassCategory.CLASS || - objectClass?.category == EtsClassCategory.OBJECT - val isFunction = scope.calcOnState { associatedFunction[ref] != null } - if (isOrdinaryInstance && !isFunction) return@with throwTypeError() - - throw UnsupportedOperationException("Unknown new constructor value: $ref") - } - override fun visit(expr: EtsNewArrayExpr): UExpr? = with(ctx) { val arrayType = expr.type diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteArray.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteArray.kt index ca0b4022f7..501b11d928 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteArray.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteArray.kt @@ -46,8 +46,7 @@ internal fun TsExprResolver.handleAssignToArrayIndex( enclosingClass = EtsClassSignature.UNKNOWN, type = EtsUnknownType, ) - assignToInstanceField(scope, lhv.array, array, field, expr, hierarchy) - return Unit + return assignToInstanceField(scope, lhv.array, array, field, expr, hierarchy) } if (receiverType !is EtsArrayType) { throw UnsupportedOperationException("Symbolic object property keys are not supported") diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt index 4c45b66f38..0c34a78733 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/WriteField.kt @@ -168,14 +168,14 @@ fun TsContext.assignToInstanceField( field: EtsFieldSignature, expr: UExpr<*>, hierarchy: EtsHierarchy, -) { +): Unit? { // Unwrap to get non-fake reference. - val unwrappedInstance = resolveHeapRef(scope, instance.unwrapRef(scope)) ?: return + val unwrappedInstance = resolveHeapRef(scope, instance.unwrapRef(scope)) ?: return null if (!isAllocatedConcreteHeapRef(unwrappedInstance) && instanceLocal.type !is EtsArrayType) { - trackInputProperty(scope, unwrappedInstance, instanceLocal, field.name, hierarchy) ?: return + trackInputProperty(scope, unwrappedInstance, instanceLocal, field.name, hierarchy) ?: return null writeInputPropertyValue(scope, unwrappedInstance, field.name, expr) - return + return Unit } val objectLiteralClass = objectLiteralClass(scope, unwrappedInstance, hierarchy) @@ -189,15 +189,16 @@ fun TsContext.assignToInstanceField( val supertype = EtsAuxiliaryType(properties = setOf(field.name)) memory.types.evalIsSubtype(unwrappedInstance, supertype) } - scope.assert(fieldExists) ?: return + scope.assert(fieldExists) ?: return null } // Determine the field sort. val sort = when { declaredObjectLiteralField != null -> typeToSort(declaredObjectLiteralField.type) newObjectLiteralField -> { - // The receiver has no declared field. A scene-wide same-name field is unrelated. - expr.sort + // An undeclared own property accepts any runtime kind. A fake wrapper + // has address storage even when its payload is a number or boolean. + if (expr.isFakeObject()) unresolvedSort else expr.sort } else -> when (val etsField = resolveEtsField(instanceLocal, field, hierarchy)) { @@ -205,7 +206,6 @@ fun TsContext.assignToInstanceField( is TsResolutionResult.Unique -> typeToSort(etsField.property.type) is TsResolutionResult.Ambiguous -> resolveAmbiguousFieldSort( scope = scope, - instanceLocal = instanceLocal, instance = unwrappedInstance, fields = etsField.properties, hierarchy = hierarchy, @@ -227,7 +227,7 @@ fun TsContext.assignToInstanceField( blockOnFalseState = { terminateAsUnsupported(reason = "Assignment changes runtime sort of field '${field.name}'") }, - ) ?: return + ) ?: return null } if (sort !is TsUnresolvedSort && expr.sort != sort && !expr.isFakeObject()) { @@ -236,7 +236,7 @@ fun TsContext.assignToInstanceField( // If the field type is unknown, we create a fake object for the expr and assign it. // Otherwise, assign expr directly. - scope.doWithState { + return scope.doWithState { if (sort is TsUnresolvedSort) { val fakeObject = expr.toFakeObject(scope) val lValue = mkFieldLValue(addressSort, unwrappedInstance, field) diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index 98a58b5eea..187c9c2c5b 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -327,6 +327,86 @@ class RuntimeNewTest : TsMethodTestRunner() { )) } + @Test + fun `field writes preserve all runtime value kinds`() { + val addedFieldOutcome = analyze(methodName = "writeAddedFieldFromBoxedValue") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, addedFieldOutcome.stopReason) + assertTrue(addedFieldOutcome.unsupportedPaths.isEmpty(), "${addedFieldOutcome.unsupportedPaths}") + + val methods = listOf( + "writeAddedFieldFromBoxedValue", + "writeTypedPrimitiveFromBoxedValue", + "writeTypedReferenceFromBoxedValue", + ) + + methods.forEach { methodName -> + withOptions(options = options.copy(stateCollectionStrategy = StateCollectionStrategy.ALL, stopOnCoverage = 0)) { + discoverProperties( + method = getMethod(methodName = methodName, className = "RuntimeNew"), + { input, result -> input.value && result.value }, + { input, result -> !input.value && result.value }, + invariants = arrayOf({ _, result -> result.value }), + ) + } + } + + replay(methods.flatMap { methodName -> + listOf( + "if (!new RuntimeNew().$methodName(true)) throw Error('$methodName(true)');", + "if (!new RuntimeNew().$methodName(false)) throw Error('$methodName(false)');", + ) + }) + } + + @Test + fun `incompatible boxed field writes stop the step and remain explicit`() { + val methods = listOf("writeIncompatibleBoxedValue", "writeIncompatibleBoxedValueByKey") + + methods.forEach { methodName -> + val method = getMethod(methodName = methodName, className = "RuntimeNew") + val outcome = analyze(methodName) + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.any { "Assignment changes runtime sort" in it }, + "$methodName: ${outcome.unsupportedPaths}") + val inputs = outcome.states.map { state -> + assertIs(state.methodResult) + val test = TsTestResolver().resolve(method, state) + assertEquals(true, assertIs(test.returnValue).value, + "$methodName: ${test.before.parameters}") + assertIs(test.before.parameters.single()).value + } + assertEquals(setOf(true), inputs.toSet(), methodName) + } + + replay(methods.flatMap { methodName -> + listOf( + "if (!new RuntimeNew().$methodName(true)) throw Error('$methodName(true)');", + "if (new RuntimeNew().$methodName(false)) throw Error('$methodName(false)');", + ) + }) + } + + @Test + fun `an object hasInstance method does not make it constructable`() { + val outcome = analyze(methodName = "nonConstructableWithHasInstance") + + assertEquals(TsAnalysisStopReason.EXHAUSTED, outcome.stopReason) + assertTrue(outcome.unsupportedPaths.isEmpty(), "${outcome.unsupportedPaths}") + assertTrue(outcome.states.isNotEmpty()) + outcome.states.forEach { state -> + val exception = assertIs(state.methodResult) + assertEquals("TypeError", exception.type.typeName) + } + + replay(listOf( + "let caught = false; try { new RuntimeNew().nonConstructableWithHasInstance(); } " + + "catch (error) { caught = error instanceof TypeError; } " + + "if (!caught) throw Error('hasInstance is not a constructor');" + )) + } + @Test fun `unmodeled constructor identity is explicitly unsupported`() { val method = getMethod(methodName = "symbolicConstructor", className = "RuntimeNew") diff --git a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts index 9316a4d17e..f618246df4 100644 --- a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts +++ b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts @@ -119,6 +119,63 @@ class RuntimeNew { return value instanceof NewA && (value as NewA).value === 7 && selected === NewB ? 1 : 0; } + writeAddedFieldFromBoxedValue(useNumber: boolean): boolean { + const source: any = { payload: useNumber ? 7 : false }; + const target: any = { marker: 1 }; + + target.value = source.payload; + + return useNumber ? target.value === 7 : target.value === false; + } + + writeTypedPrimitiveFromBoxedValue(useNumber: boolean): boolean { + const source: any = { payload: useNumber ? 7 : false }; + const target = new NewTypedFields(); + const value = source.payload; + + if (useNumber) { + target.count = value; + return target.count === 7; + } + + target.enabled = value; + return target.enabled === false; + } + + writeTypedReferenceFromBoxedValue(hasArray: boolean): boolean { + const source: any = { payload: hasArray ? [7] : undefined }; + const target = new NewTypedFields(); + + target.values = source.payload; + const alias = target.values; + + return hasArray ? alias.length === 1 && alias[0] === 7 : alias === undefined; + } + + writeIncompatibleBoxedValue(useNumber: boolean): boolean { + const source: any = { payload: useNumber ? 7 : false }; + const target = new NewTypedFields(); + + target.count = source.payload; + + return target.count === 7; + } + + writeIncompatibleBoxedValueByKey(useNumber: boolean): boolean { + const source: any = { payload: useNumber ? 7 : false }; + const target: any = new NewTypedFields(); + + target["count"] = source.payload; + + return target.count === 7; + } + + nonConstructableWithHasInstance(): boolean { + const selected: any = new NewHasInstanceObject(); + new selected(); + return false; + } + direct(): number { const value = new NewA(7); return value instanceof NewA && value.value === 7 ? 1 : 0; @@ -166,3 +223,13 @@ class RuntimeNew { return new selected(7).value; } } + +class NewTypedFields { + count: number = 0; + enabled: boolean = true; + values: number[] = []; +} + +class NewHasInstanceObject { + [Symbol.hasInstance](_value: unknown): boolean { return true; } +} From db9449c234b71b9446910d90e5f75d2d3c5a5bea Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Mon, 12 Oct 2026 14:52:01 +0300 Subject: [PATCH 8/9] Deduplicate inherited fields when choosing runtime constructor storage --- .../org/usvm/machine/expr/RuntimeFieldSort.kt | 4 +++- .../org/usvm/samples/types/RuntimeNewTest.kt | 21 +++++++++++++++++++ .../resources/samples/types/RuntimeNew.ts | 14 +++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt index 2915373740..57864a672c 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/RuntimeFieldSort.kt @@ -29,6 +29,8 @@ internal fun TsContext.resolveAmbiguousFieldSort( } ?: return unresolvedSort val ancestors = hierarchy.getAncestors(runtimeClass) - val runtimeField = fields.singleOrNull { it.declaringClass in ancestors } ?: return unresolvedSort + // Scene-wide resolution repeats a declaration for every class inheriting it. + val runtimeFields = fields.filter { it.declaringClass in ancestors }.distinct() + val runtimeField = runtimeFields.singleOrNull() ?: return unresolvedSort return typeToSort(runtimeField.type) } diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index 187c9c2c5b..116ef0a27a 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -190,6 +190,27 @@ class RuntimeNewTest : TsMethodTestRunner() { }) } + @Test + fun `inherited duplicate declarations do not change runtime field storage`() { + for (methodName in listOf("fieldReadWithInheritedDeclarations", "fieldWriteWithInheritedDeclarations")) { + val method = getMethod(methodName = methodName, className = "RuntimeNew") + + discoverProperties( + method = method, + { input, result -> input.value && result.value }, + { input, result -> !input.value && result.value }, + invariants = arrayOf({ _, result -> result.value }), + ) + } + + replay(listOf( + "if (!new RuntimeNew().fieldReadWithInheritedDeclarations(true)) throw Error('number read');", + "if (!new RuntimeNew().fieldReadWithInheritedDeclarations(false)) throw Error('string read');", + "if (!new RuntimeNew().fieldWriteWithInheritedDeclarations(true)) throw Error('number write');", + "if (!new RuntimeNew().fieldWriteWithInheritedDeclarations(false)) throw Error('string write');", + )) + } + @Test fun `runtime field sort change is reported as unsupported`() { val methodName = "writeIncompatibleRuntimeField" diff --git a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts index f618246df4..bafe92db58 100644 --- a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts +++ b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts @@ -24,6 +24,9 @@ class NewText { } } +// Inherited declarations also appear in scene-wide lookup for the parent class. +class NewAChild extends NewA {} + class ConstructorCounter { count: number; @@ -95,6 +98,17 @@ class RuntimeNew { return useNumber ? value.value === 9 : value.value === "nine"; } + fieldReadWithInheritedDeclarations(useNumber: boolean): boolean { + const value = new (useNumber ? NewA : NewText)(7); + return useNumber ? value.value === 7 : value.value === "seven"; + } + + fieldWriteWithInheritedDeclarations(useNumber: boolean): boolean { + const value = new (useNumber ? NewA : NewText)(7); + value.value = useNumber ? 11 : "eleven"; + return useNumber ? value.value === 11 : value.value === "eleven"; + } + writeIncompatibleRuntimeField(useNumber: boolean): boolean { const value = new (useNumber ? NewA : NewText)(7); value.value = "changed"; From d2ee18ad938ca581046f50d83c7aee75addf8d46 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Mon, 12 Oct 2026 15:43:11 +0300 Subject: [PATCH 9/9] [TS] Cover reference and nullish boxed field writes --- .../org/usvm/samples/types/RuntimeNewTest.kt | 3 ++ .../resources/samples/types/RuntimeNew.ts | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt index 116ef0a27a..11d96bf15a 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/samples/types/RuntimeNewTest.kt @@ -359,6 +359,9 @@ class RuntimeNewTest : TsMethodTestRunner() { "writeAddedFieldFromBoxedValue", "writeTypedPrimitiveFromBoxedValue", "writeTypedReferenceFromBoxedValue", + "writeAddedReferenceFromBoxedValue", + "writeAddedNullishFromBoxedValue", + "writeAddedFunctionFromBoxedValue", ) methods.forEach { methodName -> diff --git a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts index bafe92db58..bdf9810d9d 100644 --- a/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts +++ b/usvm-ts/src/test/resources/samples/types/RuntimeNew.ts @@ -166,6 +166,35 @@ class RuntimeNew { return hasArray ? alias.length === 1 && alias[0] === 7 : alias === undefined; } + writeAddedReferenceFromBoxedValue(hasArray: boolean): boolean { + const source: any = { payload: hasArray ? [7] : new NewA(7) }; + const target: any = { marker: 1 }; + + target.value = source.payload; + const alias = target.value; + + return hasArray ? alias.length === 1 && alias[0] === 7 : alias instanceof NewA && alias.value === 7; + } + + writeAddedNullishFromBoxedValue(useNull: boolean): boolean { + const source: any = { payload: useNull ? null : undefined }; + const target: any = { marker: 1 }; + + target.value = source.payload; + + return useNull ? target.value === null : target.value === undefined; + } + + writeAddedFunctionFromBoxedValue(useFunction: boolean): boolean { + const source: any = { payload: useFunction ? (() => 7) : "text" }; + const target: any = { marker: 1 }; + + target["value"] = source.payload; + const alias = target.value; + + return useFunction ? typeof alias === "function" && alias instanceof Function : alias === "text"; + } + writeIncompatibleBoxedValue(useNumber: boolean): boolean { const source: any = { payload: useNumber ? 7 : false }; const target = new NewTypedFields();