From ef5f5714dc85a473ededb3e91af29545a0146e51 Mon Sep 17 00:00:00 2001 From: anschnapp Date: Sun, 5 Jul 2026 00:18:48 +0200 Subject: [PATCH] fix: wrong hard coded type on ir when on compiler plugin could cause issues with double precision arithmetics --- README.md | 18 +++++++- .../mutflow/compiler/MutflowIrTransformer.kt | 4 +- .../src/main/kotlin/sample/Calculator.kt | 7 ++++ .../kotlin/sample/DoubleArithmeticTest.kt | 42 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 mutflow-test-sample/src/test/kotlin/sample/DoubleArithmeticTest.kt diff --git a/README.md b/README.md index 7b8b178..d300a6f 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,27 @@ Add the mutflow Gradle plugin to your `build.gradle.kts`: ```kotlin plugins { - kotlin("jvm") version "2.3.0" + kotlin("jvm") version "2.4.0" id("io.github.anschnapp.mutflow") version "" } ``` +### Kotlin Version Compatibility + +mutflow tracks the newest Kotlin release and updates frequently. Because compiler +plugins are tightly coupled to the compiler's internal APIs, each mutflow release +requires the Kotlin version it was built against - it will not work with older +Kotlin versions in your project. If you are on an older Kotlin, use the matching +older mutflow release: + +| mutflow | Kotlin | +|---------|--------| +| 1.1.0+ | 2.4.x | +| up to 1.0.3 | 2.2.x - 2.3.x | + +A mismatch typically fails with an obscure compiler error (e.g. `NoClassDefFoundError` +during compilation) - if you see that, check your Kotlin version first. + Once available, the plugin automatically: - Adds `mutflow-core` to your implementation dependencies (for `@MutationTarget` annotation) - Adds `mutflow-junit6` to your test dependencies (for `@MutFlowTest` annotation) diff --git a/mutflow-compiler-plugin/src/main/kotlin/io/github/anschnapp/mutflow/compiler/MutflowIrTransformer.kt b/mutflow-compiler-plugin/src/main/kotlin/io/github/anschnapp/mutflow/compiler/MutflowIrTransformer.kt index c5ff937..512fd83 100644 --- a/mutflow-compiler-plugin/src/main/kotlin/io/github/anschnapp/mutflow/compiler/MutflowIrTransformer.kt +++ b/mutflow-compiler-plugin/src/main/kotlin/io/github/anschnapp/mutflow/compiler/MutflowIrTransformer.kt @@ -473,11 +473,11 @@ class MutflowIrTransformer( call.arguments[6] = builder.irInt(occurrenceOnLine) } - // Generate when expression with inline check() calls - no temporary variable + // Generate when expression with inline check() calls - no temporary variable. return IrWhenImpl( startOffset = original.startOffset, endOffset = original.endOffset, - type = pluginContext.irBuiltIns.booleanType, + type = original.type, origin = null ).apply { variants.forEachIndexed { index, variant -> diff --git a/mutflow-test-sample/src/main/kotlin/sample/Calculator.kt b/mutflow-test-sample/src/main/kotlin/sample/Calculator.kt index 012807d..dd50cd4 100644 --- a/mutflow-test-sample/src/main/kotlin/sample/Calculator.kt +++ b/mutflow-test-sample/src/main/kotlin/sample/Calculator.kt @@ -147,4 +147,11 @@ class Calculator { fun passThroughBool(flag: Boolean): Boolean { return flag } + + // --- Double arithmetic (regression: mutation when-wrapper must keep the + // expression's type; a Boolean-typed wrapper truncated fractional Doubles) --- + + fun applyRate(amount: Double, rate: Double): Double { + return amount * rate + } } diff --git a/mutflow-test-sample/src/test/kotlin/sample/DoubleArithmeticTest.kt b/mutflow-test-sample/src/test/kotlin/sample/DoubleArithmeticTest.kt new file mode 100644 index 0000000..d14734c --- /dev/null +++ b/mutflow-test-sample/src/test/kotlin/sample/DoubleArithmeticTest.kt @@ -0,0 +1,42 @@ +package sample + +import io.github.anschnapp.mutflow.MutFlow +import io.github.anschnapp.mutflow.MutationRegistry +import io.github.anschnapp.mutflow.Selection +import io.github.anschnapp.mutflow.Shuffle +import kotlin.test.* + +/** + * Regression test: mutated Double arithmetic must not lose precision. + * + * The mutation when-wrapper around IrCall nodes was typed Boolean regardless of + * the original expression's type. For Double-valued expressions the JVM backend + * then coerced the result, silently truncating fractional values even with no + * mutation active (50.0 * 0.05 returned 2.0 instead of 2.5). Every other test + * in this module uses integer-valued results, which is why only a fractional + * result catches this. + */ +class DoubleArithmeticTest { + + private val calculator = Calculator() + + @BeforeTest + fun setup() { + MutationRegistry.reset() + MutFlow.reset() + } + + @Test + fun `fractional double result survives instrumentation outside a session`() { + // No session at all: check() returns null, so the original code path runs. + assertEquals(2.5, calculator.applyRate(50.0, 0.05)) + } + + @Test + fun `fractional double result survives instrumentation in baseline`() { + val result = MutFlow.underTest(run = 0, selection = Selection.MostLikelyStable, shuffle = Shuffle.PerChange) { + calculator.applyRate(50.0, 0.05) + } + assertEquals(2.5, result) + } +}