Skip to content

[TS] Mock unresolved virtual calls instead of killing the state#342

Draft
CaelmBleidd wants to merge 3 commits into
mainfrom
caelmbleidd/ts-interpreter-fixes
Draft

[TS] Mock unresolved virtual calls instead of killing the state#342
CaelmBleidd wants to merge 3 commits into
mainfrom
caelmbleidd/ts-interpreter-fixes

Conversation

@CaelmBleidd

Copy link
Copy Markdown
Member

Coverage investigation on an open-source corpus showed that 155 of 172 unreached symbolic targets died in under 100ms: TsInterpreter killed the state (assert(false)) on ANY unresolved callee (e.g. Number.isInteger), making everything after such a call unreachable. All three unresolved-call sites in visitVirtualMethodCall now fall back to mockMethodCall + returnSite, mirroring the pre-existing unresolved-constructor approximation. Full usvm-ts suite stays green (422 tests).

NOTE: an engine fix, staged to be extracted into a separate PR.

Coverage investigation on an open-source corpus showed that 155 of 172
unreached symbolic targets died in under 100ms: TsInterpreter killed the
state (assert(false)) on ANY unresolved callee (e.g. Number.isInteger),
making everything after such a call unreachable. All three unresolved-call
sites in visitVirtualMethodCall now fall back to mockMethodCall +
returnSite, mirroring the pre-existing unresolved-constructor
approximation. Full usvm-ts suite stays green (422 tests).

NOTE: an engine fix, staged to be extracted into a separate PR.
@CaelmBleidd CaelmBleidd marked this pull request as draft July 8, 2026 09:28
Front ends lower the side effect of `x++`/`--x` into an explicit
assignment (the native ts-frontend emits `%t := x; x := ++x`, ArkAnalyzer
expands to `x := x + 1`), and the jacodb DTO conversion maps both `++` and
`--` to the Pre* model classes only. The resolver previously crashed on
them ("Not supported"), killing every state in loops written with
`x--`/`--x` under the ts-frontend. Pre* now evaluate to ToNumber(arg) +- 1
and Post* (never produced by the converter, but part of the model) to the
old value.

Tests: direct machine tests over programmatically built IR (ArkAnalyzer
cannot exercise these visits since it never emits the unary forms) plus an
ArkAnalyzer-lowered sample. A `const old = x++` sample case is deliberately
absent: ArkAnalyzer lowers it as `x := x + 1; old := x`, i.e. `old`
receives the new value - a frontend lowering bug found while writing these
tests.
@Test
fun `pre-increment yields arg plus one`() {
for ((input, result) in run("preInc") { EtsPreIncExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
fun `pre-increment yields arg plus one`() {
for ((input, result) in run("preInc") { EtsPreIncExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
else assertEquals(input + 1, result, 0.0) { "++($input)" }
@Test
fun `pre-decrement yields arg minus one`() {
for ((input, result) in run("preDec") { EtsPreDecExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
fun `pre-decrement yields arg minus one`() {
for ((input, result) in run("preDec") { EtsPreDecExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
else assertEquals(input - 1, result, 0.0) { "--($input)" }
@Test
fun `post-increment and post-decrement yield the old value`() {
for ((input, result) in run("postInc") { EtsPostIncExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
fun `post-increment and post-decrement yield the old value`() {
for ((input, result) in run("postInc") { EtsPostIncExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
else assertEquals(input, result, 0.0) { "($input)++" }
else assertEquals(input, result, 0.0) { "($input)++" }
}
for ((input, result) in run("postDec") { EtsPostDecExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
}
for ((input, result) in run("postDec") { EtsPostDecExpr(it, EtsNumberType) }) {
if (input.isNaN()) assertTrue(result.isNaN())
else assertEquals(input, result, 0.0) { "($input)--" }
…lback

Frequency data from open-source corpora (TheAlgorithms/TypeScript maths and
loiane/javascript-datastructures-algorithms, full-log probes) shows that
unresolved calls split into three groups: in-project cross-file targets
(a scene-construction problem, not the engine's), a small set of very hot
builtins (Math.abs — 208 occurrences, Number.isInteger — 55, Math.sqrt — 13
on the maths corpus alone), and a tail of genuinely unmodelable calls.

This change shrinks the over-approximation surface of the mock fallback by
modeling the hot builtins precisely in the fp theory:
- Number.isInteger / isSafeInteger (2^53 bound not modeled) / isFinite,
  with the same fake-object discriminator handling as Number.isNaN
- Math.floor/ceil/trunc via fp round-to-integral in the matching mode
  (floor generalized), Math.round as floor(x + 0.5) per JS semantics,
  Math.abs, Math.sqrt
- Math.min/max with JS NaN semantics (NaN if any argument is NaN, unlike
  IEEE minNum/maxNum)
- console.* treated as side-effect-free undefined, like Logger

Every remaining mock fallback now logs a distinct
"Mocking an unresolved call: <callee> (over-approximation)" line, so the
residual over-approximation stays measurable on corpus runs.

decrementLoop test properties are reclassified by the observed loop depth:
non-integral inputs (n = 2.5) legitimately reach depth ceil(n), and the
solver started picking such models once the approximations changed the
search.
"Math.${expr.callee.name}() should have exactly one argument, but got ${expr.args.size}"
}
val arg = resolve(expr.args.single()) ?: return null
when {
val plusHalf = mkFpAddExpr(
mkFpRoundingModeExpr(KFpRoundingMode.RoundNearestTiesToEven),
value,
mkFp64(0.5),
private fun TsExprResolver.handleMathMinMax(expr: EtsInstanceCallExpr, isMin: Boolean): UExpr<*>? = with(ctx) {
val args = expr.args.map { arg ->
val resolved = resolve(arg) ?: return null
when {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants