11package org.usvm.machine.expr
22
33import io.ksmt.expr.KFpRoundingMode
4+ import io.ksmt.sort.KFp64Sort
45import io.ksmt.utils.asExpr
56import mu.KotlinLogging
67import org.jacodb.ets.model.EtsArrayType
@@ -28,6 +29,7 @@ import org.usvm.types.first
2829import org.usvm.types.firstOrNull
2930import org.usvm.util.mkArrayIndexLValue
3031import org.usvm.util.mkArrayLengthLValue
32+ import org.usvm.util.boolToFp
3133import org.usvm.util.resolveEtsMethods
3234
3335private val logger = KotlinLogging .logger {}
@@ -58,6 +60,17 @@ internal fun TsExprResolver.tryApproximateInstanceCall(
5860 if (expr.callee.name == " isNaN" ) {
5961 return from(handleNumberIsNaN(expr))
6062 }
63+ if (expr.callee.name == " isInteger" || expr.callee.name == " isSafeInteger" ) {
64+ return from(handleNumberIsInteger(expr))
65+ }
66+ if (expr.callee.name == " isFinite" ) {
67+ return from(handleNumberIsFinite(expr))
68+ }
69+ }
70+
71+ // `console.*` is side-effect-free for the analysis, like `Logger`
72+ if (expr.instance.name == " console" ) {
73+ return from(mkUndefinedValue())
6174 }
6275
6376 // Handle 'Boolean' constructor calls
@@ -79,8 +92,15 @@ internal fun TsExprResolver.tryApproximateInstanceCall(
7992
8093 // Handle `Math` method calls
8194 if (expr.instance.name == " Math" ) {
82- if (expr.callee.name == " floor" ) {
83- return from(handleMathFloor(expr))
95+ when (expr.callee.name) {
96+ " floor" -> return from(handleMathRounding(expr, KFpRoundingMode .RoundTowardNegative ))
97+ " ceil" -> return from(handleMathRounding(expr, KFpRoundingMode .RoundTowardPositive ))
98+ " trunc" -> return from(handleMathRounding(expr, KFpRoundingMode .RoundTowardZero ))
99+ " round" -> return from(handleMathRound(expr))
100+ " abs" -> return from(handleMathAbs(expr))
101+ " sqrt" -> return from(handleMathSqrt(expr))
102+ " min" -> return from(handleMathMinMax(expr, isMin = true ))
103+ " max" -> return from(handleMathMinMax(expr, isMin = false ))
84104 }
85105 }
86106
@@ -263,27 +283,152 @@ private fun TsExprResolver.handlePromiseResolveReject(expr: EtsInstanceCallExpr)
263283 promise
264284}
265285
266- private fun TsExprResolver.handleMathFloor (expr : EtsInstanceCallExpr ): UExpr <* >? = with (ctx) {
286+ /* * Resolve the single argument of a Math function as an fp64 value, when possible. */
287+ private fun TsExprResolver.resolveSingleFpArg (expr : EtsInstanceCallExpr ): UExpr <KFp64Sort >? = with (ctx) {
267288 check(expr.args.size == 1 ) {
268- " Math.floor () should have exactly one argument, but got ${expr.args.size} "
289+ " Math.${expr.callee.name} () should have exactly one argument, but got ${expr.args.size} "
269290 }
270291 val arg = resolve(expr.args.single()) ? : return null
292+ when {
293+ arg.isFakeObject() -> {
294+ // Constrain the fake object to its numeric alternative: precise Math
295+ // semantics for non-number arguments (ToNumber coercions) is future work.
296+ scope.assert (arg.getFakeType(scope).fpTypeExpr) ? : return null
297+ arg.extractFp(scope)
298+ }
271299
272- when (arg.sort) {
273- fp64Sort -> mkFpRoundToIntegralExpr(
274- roundingMode = mkFpRoundingModeExpr(KFpRoundingMode .RoundTowardNegative ),
275- value = arg.asExpr(fp64Sort),
276- )
300+ arg.sort == fp64Sort -> arg.asExpr(fp64Sort)
277301
278- sizeSort -> arg.asExpr(sizeSort )
302+ arg.sort == boolSort -> boolToFp( arg.asExpr(boolSort) )
279303
280304 else -> {
281- logger.warn { " Unsupported argument sort for Math.floor (): ${arg.sort} " }
305+ logger.warn { " Unsupported argument sort for Math.${expr.callee.name} (): ${arg.sort} " }
282306 null
283307 }
284308 }
285309}
286310
311+ /* * `Math.floor` / `Math.ceil` / `Math.trunc`: fp round-to-integral in the corresponding mode. */
312+ private fun TsExprResolver.handleMathRounding (
313+ expr : EtsInstanceCallExpr ,
314+ mode : KFpRoundingMode ,
315+ ): UExpr <* >? = with (ctx) {
316+ val value = resolveSingleFpArg(expr) ? : return null
317+ mkFpRoundToIntegralExpr(mkFpRoundingModeExpr(mode), value)
318+ }
319+
320+ /* *
321+ * `Math.round(x)` in JS is `floor(x + 0.5)` (halfway cases round towards +Infinity),
322+ * which differs from the IEEE round-to-nearest-even mode.
323+ * (The sign of a `-0` result is not preserved by this encoding.)
324+ */
325+ private fun TsExprResolver.handleMathRound (expr : EtsInstanceCallExpr ): UExpr <* >? = with (ctx) {
326+ val value = resolveSingleFpArg(expr) ? : return null
327+ val plusHalf = mkFpAddExpr(
328+ mkFpRoundingModeExpr(KFpRoundingMode .RoundNearestTiesToEven ),
329+ value,
330+ mkFp64(0.5 ),
331+ )
332+ mkFpRoundToIntegralExpr(mkFpRoundingModeExpr(KFpRoundingMode .RoundTowardNegative ), plusHalf)
333+ }
334+
335+ private fun TsExprResolver.handleMathAbs (expr : EtsInstanceCallExpr ): UExpr <* >? = with (ctx) {
336+ val value = resolveSingleFpArg(expr) ? : return null
337+ mkFpAbsExpr(value)
338+ }
339+
340+ private fun TsExprResolver.handleMathSqrt (expr : EtsInstanceCallExpr ): UExpr <* >? = with (ctx) {
341+ val value = resolveSingleFpArg(expr) ? : return null
342+ mkFpSqrtExpr(mkFpRoundingModeExpr(KFpRoundingMode .RoundNearestTiesToEven ), value)
343+ }
344+
345+ /* *
346+ * `Math.min` / `Math.max`. Unlike the IEEE minNum/maxNum operations (which prefer
347+ * the non-NaN operand), JS returns NaN if *any* argument is NaN.
348+ */
349+ private fun TsExprResolver.handleMathMinMax (expr : EtsInstanceCallExpr , isMin : Boolean ): UExpr <* >? = with (ctx) {
350+ val args = expr.args.map { arg ->
351+ val resolved = resolve(arg) ? : return null
352+ when {
353+ resolved.isFakeObject() -> {
354+ scope.assert (resolved.getFakeType(scope).fpTypeExpr) ? : return null
355+ resolved.extractFp(scope)
356+ }
357+
358+ resolved.sort == fp64Sort -> resolved.asExpr(fp64Sort)
359+ resolved.sort == boolSort -> boolToFp(resolved.asExpr(boolSort))
360+ else -> {
361+ logger.warn { " Unsupported argument sort for Math.min/max: ${resolved.sort} " }
362+ return null
363+ }
364+ }
365+ }
366+ if (args.isEmpty()) {
367+ // Math.min() == +Infinity, Math.max() == -Infinity
368+ return mkFp64(if (isMin) Double .POSITIVE_INFINITY else Double .NEGATIVE_INFINITY )
369+ }
370+ val anyNaN = args.map { mkFpIsNaNExpr(it) }.reduce { a, b -> mkOr(a, b) }
371+ val pure = args.reduce { a, b -> if (isMin) mkFpMinExpr(a, b) else mkFpMaxExpr(a, b) }
372+ mkIte(anyNaN, mkFp64(Double .NaN ), pure)
373+ }
374+
375+ /* *
376+ * 21.1.2.3 `Number.isInteger ( number )`: false for non-numbers, NaN and infinities;
377+ * true iff the value equals its integral rounding. `isSafeInteger` is approximated
378+ * the same way (the 2^53 bound is not modeled).
379+ */
380+ private fun TsExprResolver.handleNumberIsInteger (expr : EtsInstanceCallExpr ): UExpr <* >? = with (ctx) {
381+ check(expr.args.size == 1 ) { " Number.isInteger should have one argument" }
382+ val arg = resolve(expr.args.single()) ? : return null
383+
384+ fun isIntegral (value : UExpr <KFp64Sort >): UBoolExpr {
385+ val rounded = mkFpRoundToIntegralExpr(mkFpRoundingModeExpr(KFpRoundingMode .RoundTowardZero ), value)
386+ return mkAnd(
387+ mkNot(mkFpIsInfiniteExpr(value)),
388+ mkFpEqualExpr(rounded, value), // false for NaN by IEEE equality
389+ )
390+ }
391+
392+ if (arg.isFakeObject()) {
393+ val fakeType = arg.getFakeType(scope)
394+ return mkIte(
395+ condition = fakeType.fpTypeExpr,
396+ trueBranch = isIntegral(arg.extractFp(scope)),
397+ falseBranch = mkFalse(),
398+ )
399+ }
400+
401+ if (arg.sort == fp64Sort) {
402+ isIntegral(arg.asExpr(fp64Sort))
403+ } else {
404+ mkFalse()
405+ }
406+ }
407+
408+ /* * 21.1.2.2 `Number.isFinite ( number )`: false for non-numbers, NaN and infinities. */
409+ private fun TsExprResolver.handleNumberIsFinite (expr : EtsInstanceCallExpr ): UExpr <* >? = with (ctx) {
410+ check(expr.args.size == 1 ) { " Number.isFinite should have one argument" }
411+ val arg = resolve(expr.args.single()) ? : return null
412+
413+ fun isFinite (value : UExpr <KFp64Sort >): UBoolExpr =
414+ mkAnd(mkNot(mkFpIsNaNExpr(value)), mkNot(mkFpIsInfiniteExpr(value)))
415+
416+ if (arg.isFakeObject()) {
417+ val fakeType = arg.getFakeType(scope)
418+ return mkIte(
419+ condition = fakeType.fpTypeExpr,
420+ trueBranch = isFinite(arg.extractFp(scope)),
421+ falseBranch = mkFalse(),
422+ )
423+ }
424+
425+ if (arg.sort == fp64Sort) {
426+ isFinite(arg.asExpr(fp64Sort))
427+ } else {
428+ mkFalse()
429+ }
430+ }
431+
287432/* *
288433 * Handles the `Array.push(...items)` method call.
289434 * Appends the specified `items` to the end of the array.
0 commit comments