Skip to content

Commit e73af2a

Browse files
committed
Fix Array.concat approximation
1 parent 090ceef commit e73af2a

3 files changed

Lines changed: 62 additions & 41 deletions

File tree

usvm-ts/src/main/kotlin/org/usvm/machine/expr/CallApproximations.kt

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import org.usvm.machine.interpreter.PromiseState
2424
import org.usvm.machine.interpreter.markResolved
2525
import org.usvm.machine.interpreter.setResolvedValue
2626
import org.usvm.sizeSort
27+
import org.usvm.types.first
2728
import org.usvm.types.firstOrNull
2829
import org.usvm.util.mkArrayIndexLValue
2930
import org.usvm.util.mkArrayLengthLValue
@@ -546,7 +547,6 @@ private fun TsExprResolver.handleArrayJoin(
546547
* The `start` index is inclusive, and the `end` index is exclusive.
547548
* If `start` is not provided, it defaults to `0`.
548549
* If `end` is not provided, it defaults to the length of the array.
549-
* This method returns the sliced array.
550550
*
551551
* https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.slice
552552
*/
@@ -566,6 +566,7 @@ private fun TsExprResolver.handleArraySlice(
566566
}
567567
val startBv: UExpr<TsSizeSort> = when (start.sort) {
568568
sizeSort -> start.asExpr(sizeSort)
569+
569570
fp64Sort -> mkFpToBvExpr(
570571
roundingMode = fpRoundingModeSortDefaultValue(),
571572
value = start.asExpr(fp64Sort),
@@ -586,6 +587,7 @@ private fun TsExprResolver.handleArraySlice(
586587
}
587588
val endBv: UExpr<TsSizeSort> = when (end.sort) {
588589
sizeSort -> end.asExpr(sizeSort)
590+
589591
fp64Sort -> mkFpToBvExpr(
590592
roundingMode = fpRoundingModeSortDefaultValue(),
591593
value = end.asExpr(fp64Sort),
@@ -623,9 +625,20 @@ private fun TsExprResolver.handleArraySlice(
623625
}
624626

625627
/**
626-
* Handles the `Array.concat(...arrays)` method call.
627-
* Concatenates the specified `arrays` to the end of the current array.
628-
* This method modifies the array in place and returns the modified array.
628+
* Handles the `Array.concat(...items)` method call.
629+
* Returns a new array containing the elements of the original array followed by the array elements of each argument.
630+
*
631+
* ### Examples:
632+
* ```
633+
* let a = [1, 2];
634+
* let b = [3, 4];
635+
*
636+
* a.concat() -> [1, 2]
637+
* a.concat(b) -> [1, 2, 3, 4]
638+
* a.concat(b, 5) -> [1, 2, 3, 4, 5]
639+
*
640+
* a == [1, 2] // not modified
641+
* ```
629642
*
630643
* https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.concat
631644
*/
@@ -641,37 +654,44 @@ private fun TsExprResolver.handleArrayConcat(
641654

642655
val args = expr.args.map { resolve(it) ?: return null }
643656

657+
logger.warn {
658+
"Array.concat() is not fully implemented, returning a symbolic array approximation"
659+
}
660+
644661
scope.calcOnState {
645-
// Read the length of the array
646-
val lengthLValue = mkArrayLengthLValue(array, arrayType)
647-
val length = scope.calcOnState { memory.read(lengthLValue) }
648-
649-
// Iterate over the provided arrays and concatenate them
650-
args.forEach { arg ->
651-
val otherArray = arg.asExpr(addressSort)
652-
653-
// Read the length of the argument array
654-
val argLengthLValue = mkArrayLengthLValue(otherArray, arrayType)
655-
val argLength = memory.read(argLengthLValue)
656-
657-
// Copy the elements from the argument array to the end of the current array
658-
memory.memcpy(
659-
srcRef = otherArray,
660-
dstRef = array,
661-
type = arrayType,
662-
elementSort = elementSort,
663-
fromSrc = 0.toBv().asExpr(sizeSort),
664-
fromDst = length,
665-
length = argLength,
666-
)
667-
668-
// Update the length of the current array
669-
val newLength = mkBvAddExpr(length, argLength)
670-
scope.doWithState { memory.write(lengthLValue, newLength, guard = trueExpr) }
662+
// Allocate a new array for the concatenated result
663+
val resultArray = memory.allocConcrete(arrayType)
664+
665+
// Read the length of the original array
666+
val originalLengthLValue = mkArrayLengthLValue(array, arrayType)
667+
val originalLength = memory.read(originalLengthLValue)
668+
669+
// Calculate the total length of the result
670+
var totalLength = originalLength
671+
for (arg in args) {
672+
// For each array-like argument, add its length to the total length
673+
if (arg.sort == addressSort) {
674+
// TODO: handle empty type stream
675+
val argType = memory.typeStreamOf(arg.asExpr(addressSort)).first()
676+
if (argType is EtsArrayType) {
677+
val argLengthLValue = mkArrayLengthLValue(arg.asExpr(addressSort), argType)
678+
val argLength = memory.read(argLengthLValue)
679+
totalLength = mkBvAddExpr(totalLength, argLength)
680+
continue
681+
}
682+
}
683+
// For non-array arguments, treat them as a single element
684+
totalLength = mkBvAddExpr(totalLength, 1.toBv())
671685
}
672686

673-
// Return the modified array
674-
array
687+
// TODO: fill the resultArray
688+
689+
// Set the length of the result array
690+
val resultLengthLValue = mkArrayLengthLValue(resultArray, arrayType)
691+
memory.write(resultLengthLValue, totalLength, guard = trueExpr)
692+
693+
// Return the new concatenated array (with symbolic content)
694+
resultArray
675695
}
676696
}
677697

@@ -699,7 +719,7 @@ private fun TsExprResolver.handleArrayIndexOf(
699719
val length = memory.read(lengthLValue)
700720

701721
// Create a symbolic index that is either -1 (not found) or a valid index
702-
val symbolicResult = memory.mocker.createMockSymbol(null, sizeSort, ownership)
722+
val symbolicResult = makeSymbolicPrimitive(sizeSort)
703723

704724
// Add constraints: result is either -1 or in range [0, length-1]
705725
val notFound = mkEq(symbolicResult, mkBv(-1, sizeSort))

usvm-ts/src/test/kotlin/org/usvm/samples/arrays/ArrayMethods.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,14 @@ class ArrayMethods : TsMethodTestRunner() {
120120
discoverProperties<TsTestValue.TsBoolean, TsTestValue>(
121121
method = method,
122122
{ x, r ->
123-
x.value && r is TsTestValue.TsArray<*>
124-
// Note: Current implementation modifies array in-place (spec deviation)
123+
// if (x) then [1, 2, 3, 4]
124+
x.value && r is TsTestValue.TsArray<*> && r.values.map { it as TsTestValue.TsNumber }
125+
.map { it.number } == listOf(1.0, 2..0, 3.0, 4.0)
125126
},
126127
{ x, r ->
127-
!x.value && r is TsTestValue.TsArray<*>
128-
// Note: Should return new array according to spec
128+
// if (!x) then [1, 2]
129+
x.value && r is TsTestValue.TsArray<*> && r.values.map { it as TsTestValue.TsNumber }
130+
.map { it.number } == listOf(1.0, 2..0)
129131
},
130132
)
131133
}

usvm-ts/src/test/resources/samples/arrays/ArrayMethods.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,11 @@ class ArrayMethods {
6969
arrayConcat(x: boolean) {
7070
const arr1 = [1, 2];
7171
const arr2 = [3, 4];
72+
const result = arr1.concat(arr2);
7273
if (x) {
73-
arr1.concat(arr2);
74-
return arr1; // [1, 2, 3, 4] - modified in place
75-
} else {
76-
const result = arr1.concat(arr2);
7774
return result; // [1, 2, 3, 4]
75+
} else {
76+
return arr1; // [1, 2]
7877
}
7978
}
8079

0 commit comments

Comments
 (0)