Describe the Issue
A native image built with -O1 throws a NullPointerException while JetBrains Compose Runtime processes a state invalidation.
The same application works correctly:
- on the JVM;
- as a native image built with
-O0.
The reproducer has a runtime switch that calls Snapshot.sendApplyNotifications(). This makes it possible to demonstrate the failure using the same -O1 executable:
Sample.exe
-> PASS
Sample.exe notify
-> NullPointerException at androidx.compose.runtime.collection.ScopeMap.set-impl(ScopeMap.kt:59)
The application does not use terminal handling, FFM, or application JNI/native libraries. The reproducer only uses Compose Runtime and kotlinx.coroutines.
The exception occurs after Compose has observed a MutableState, the state is modified, and pending snapshot changes are delivered to the Recomposer.
Using the latest version of GraalVM can resolve many issues.
GraalVM Version
openjdk version "25.0.4" 2026-07-21
OpenJDK Runtime Environment GraalVM CE 25.2.4+7.1 (build 25.0.4+7-jvmci-25.2-b20)
OpenJDK 64-Bit Server VM GraalVM CE 25.2.4+7.1 (build 25.0.4+7-jvmci-25.2-b20, mixed mode, sharing)
Native Image build output reports:
Builder configuration:
- Java version: 25.0.4+7, vendor version: GraalVM CE 25.2.4+7.1
- Graal compiler: optimization level: 1, target machine: x86-64-v3
- C compiler: cl.exe (microsoft, x64, 19.51.36256)
The same -O1 failure was also reproduced with several other GraalVM 25 builds, including GraalVM CE 25+37.1, GraalVM CE 25.1.3, and Oracle GraalVM 25.0.4.
Operating System and Version
Microsoft Windows [Version 10.0.26300.9032]
Troubleshooting Confirmation
Run Command
Build once with -O1:
.\gradlew.bat clean nativeCompile -PO1 --no-daemon
Run the same executable without snapshot notification:
.\build\native\nativeCompile\Sample.exe
Run the same executable with snapshot notification:
.\build\native\nativeCompile\Sample.exe notify
Expected Behavior
Both invocations of the same native executable should complete without an exception, as they do on the JVM and with Native Image -O0.
Expected output for the notify invocation:
compose: 0
before write
after write
done
Actual Behavior
The -O1 executable succeeds when run without the notify argument:
compose: 0
before write
after write
done
The same executable fails when run with notify:
compose: 0
before write
after write
Exception in thread "main" java.lang.NullPointerException
at androidx.compose.runtime.collection.ScopeMap.set-impl(ScopeMap.kt:59)
at androidx.compose.runtime.CompositionImpl.invalidateChecked(Composition.kt:1232)
at androidx.compose.runtime.CompositionImpl.invalidate(Composition.kt:1183)
at androidx.compose.runtime.RecomposeScopeImpl.invalidateForResult(RecomposeScopeImpl.kt:257)
at androidx.compose.runtime.CompositionImpl.addPendingInvalidationsLocked(Composition.kt:897)
at androidx.compose.runtime.CompositionImpl.addPendingInvalidationsLocked(Composition.kt:913)
at androidx.compose.runtime.CompositionImpl.drainPendingModificationsLocked(Composition.kt:743)
at androidx.compose.runtime.CompositionImpl.recordModificationsOf(Composition.kt:877)
at androidx.compose.runtime.Recomposer.recordComposerModifications(Recomposer.kt:449)
at androidx.compose.runtime.Recomposer.access$recordComposerModifications(Recomposer.kt:142)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.invokeSuspend(Recomposer.kt:570)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:263)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:94)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:70)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at org.ttldtor.Main.main(Main.kt:30)
The control matrix is:
| Runtime |
no argument |
notify |
| JVM |
PASS |
PASS |
Native Image -O0 |
PASS |
PASS |
Native Image -O1 |
PASS |
NPE |
Steps to Reproduce
Create a Gradle Kotlin/JVM project with the following files.
settings.gradle.kts
rootProject.name = "Sample"
build.gradle.kts
Source
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.2.10"
application
id("org.graalvm.buildtools.native") version "1.1.9"
id("org.jetbrains.kotlin.plugin.compose") version "2.2.10"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation("org.jetbrains.compose.runtime:runtime:1.8.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("androidx.collection:collection-jvm:1.5.0")
}
kotlin {
jvmToolchain(24)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_24)
}
}
java {
sourceCompatibility = JavaVersion.VERSION_24
targetCompatibility = JavaVersion.VERSION_24
}
application {
mainClass = "org.ttldtor.Main"
}
graalvmNative {
metadataRepository {
enabled.set(false)
}
binaries {
named("main") {
imageName.set("Sample")
mainClass.set("org.ttldtor.Main")
if (providers.gradleProperty("O0").isPresent) {
buildArgs.add("-O0")
}
if (providers.gradleProperty("O1").isPresent) {
buildArgs.add("-O1")
}
}
}
}
src/main/kotlin/org/ttldtor/Main.kt
Source
@file:JvmName("Main")
package org.ttldtor
import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.runtime.Composition
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.Snapshot
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import kotlin.jvm.JvmName
private class UnitApplier : AbstractApplier<Unit>(Unit) {
override fun insertTopDown(index: Int, instance: Unit) {}
override fun insertBottomUp(index: Int, instance: Unit) {}
override fun remove(index: Int, count: Int) {}
override fun move(from: Int, to: Int, count: Int) {}
override fun onClear() {}
}
private val state = mutableStateOf(0)
fun main(args: Array<String>) {
val clock = BroadcastFrameClock()
runBlocking(clock) {
val recomposer = Recomposer(coroutineContext)
val composition = Composition(UnitApplier(), recomposer)
val recomposerJob =
launch(start = UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
composition.setContent {
println("compose: ${state.value}")
}
println("before write")
state.value = 1
println("after write")
if (args.firstOrNull() == "notify") {
Snapshot.sendApplyNotifications()
}
yield()
recomposer.cancel()
recomposer.join()
recomposerJob.join()
println("done")
}
}
A JDK 24 toolchain is used to compile the Kotlin 2.2.10 sources to JVM 24 bytecode. GraalVM 25.2.4 is used by the Native Image Gradle task.
Reproduce the failure:
.\gradlew.bat clean nativeCompile -PO1 --no-daemon
.\build\native\nativeCompile\Sample.exe
.\build\native\nativeCompile\Sample.exe notify
The first invocation passes; the second invocation of the same executable throws the NPE above.
Control with -O0:
.\gradlew.bat clean nativeCompile -PO0 --no-daemon
.\build\native\nativeCompile\Sample.exe
.\build\native\nativeCompile\Sample.exe notify
Both invocations pass.
Control on the JVM:
.\gradlew.bat clean run --no-daemon
.\gradlew.bat run --args=notify --no-daemon
Both invocations pass.
Additional Context
The reproducer was minimized from an application that originally used Mosaic, but Mosaic is not required to reproduce the issue and is not present in the code above.
The failure requires Compose to observe mutable state and then process the corresponding snapshot modification. Merely creating/writing MutableState without an observed composition does not reproduce it.
Calling Snapshot.sendApplyNotifications() is a runtime trigger, not necessarily the location of the miscompilation. The failing path is:
Snapshot.sendApplyNotifications()
-> Recomposer.recordComposerModifications()
-> CompositionImpl.recordModificationsOf()
-> CompositionImpl.invalidateChecked()
-> ScopeMap.set()
-> NullPointerException
During minimization I also tried disabling several individual Native Image optimizations at -O1, including trivial AOT inlining, regular and early read elimination, partial escape analysis, conditional elimination, and inlining. These experiments did not make the -O1 failure disappear.
The ScopeMap code in Compose Runtime is a Kotlin value class wrapping an AndroidX MutableScatterMap. A standalone reduced test using a similar value class and MutableScatterMap does not reproduce the issue, so the failure appears to require the actual Compose invalidation path rather than the collection/value-class construct alone.
Run-Time Log Output and Error Messages
The minimal relevant output from GraalVM CE 25.2.4+7.1 is:
Builder configuration:
- Java version: 25.0.4+7, vendor version: GraalVM CE 25.2.4+7.1
- Graal compiler: optimization level: 1, target machine: x86-64-v3
- C compiler: cl.exe (microsoft, x64, 19.51.36256)
Then, for the same generated Sample.exe:
> .\build\native\nativeCompile\Sample.exe
compose: 0
before write
after write
done
> .\build\native\nativeCompile\Sample.exe notify
compose: 0
before write
after write
Exception in thread "main" java.lang.NullPointerException
at androidx.compose.runtime.collection.ScopeMap.set-impl(ScopeMap.kt:59)
at androidx.compose.runtime.CompositionImpl.invalidateChecked(Composition.kt:1232)
at androidx.compose.runtime.CompositionImpl.invalidate(Composition.kt:1183)
at androidx.compose.runtime.RecomposeScopeImpl.invalidateForResult(RecomposeScopeImpl.kt:257)
at androidx.compose.runtime.CompositionImpl.addPendingInvalidationsLocked(Composition.kt:897)
at androidx.compose.runtime.CompositionImpl.addPendingInvalidationsLocked(Composition.kt:913)
at androidx.compose.runtime.CompositionImpl.drainPendingModificationsLocked(Composition.kt:743)
at androidx.compose.runtime.CompositionImpl.recordModificationsOf(Composition.kt:877)
at androidx.compose.runtime.Recomposer.recordComposerModifications(Recomposer.kt:449)
at androidx.compose.runtime.Recomposer.access$recordComposerModifications(Recomposer.kt:142)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.invokeSuspend(Recomposer.kt:570)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:263)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:94)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:70)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at org.ttldtor.Main.main(Main.kt:30)
Describe the Issue
A native image built with
-O1throws aNullPointerExceptionwhile JetBrains Compose Runtime processes a state invalidation.The same application works correctly:
-O0.The reproducer has a runtime switch that calls
Snapshot.sendApplyNotifications(). This makes it possible to demonstrate the failure using the same-O1executable:The application does not use terminal handling, FFM, or application JNI/native libraries. The reproducer only uses Compose Runtime and kotlinx.coroutines.
The exception occurs after Compose has observed a
MutableState, the state is modified, and pending snapshot changes are delivered to theRecomposer.Using the latest version of GraalVM can resolve many issues.
GraalVM Version
Native Image build output reports:
The same
-O1failure was also reproduced with several other GraalVM 25 builds, including GraalVM CE 25+37.1, GraalVM CE 25.1.3, and Oracle GraalVM 25.0.4.Operating System and Version
Microsoft Windows [Version 10.0.26300.9032]
Troubleshooting Confirmation
Run Command
Build once with
-O1:Run the same executable without snapshot notification:
.\build\native\nativeCompile\Sample.exeRun the same executable with snapshot notification:
.\build\native\nativeCompile\Sample.exe notifyExpected Behavior
Both invocations of the same native executable should complete without an exception, as they do on the JVM and with Native Image
-O0.Expected output for the
notifyinvocation:Actual Behavior
The
-O1executable succeeds when run without thenotifyargument:The same executable fails when run with
notify:The control matrix is:
notify-O0-O1Steps to Reproduce
Create a Gradle Kotlin/JVM project with the following files.
settings.gradle.ktsbuild.gradle.ktsSource
src/main/kotlin/org/ttldtor/Main.ktSource
A JDK 24 toolchain is used to compile the Kotlin 2.2.10 sources to JVM 24 bytecode. GraalVM 25.2.4 is used by the Native Image Gradle task.
Reproduce the failure:
The first invocation passes; the second invocation of the same executable throws the NPE above.
Control with
-O0:Both invocations pass.
Control on the JVM:
Both invocations pass.
Additional Context
The reproducer was minimized from an application that originally used Mosaic, but Mosaic is not required to reproduce the issue and is not present in the code above.
The failure requires Compose to observe mutable state and then process the corresponding snapshot modification. Merely creating/writing
MutableStatewithout an observed composition does not reproduce it.Calling
Snapshot.sendApplyNotifications()is a runtime trigger, not necessarily the location of the miscompilation. The failing path is:During minimization I also tried disabling several individual Native Image optimizations at
-O1, including trivial AOT inlining, regular and early read elimination, partial escape analysis, conditional elimination, and inlining. These experiments did not make the-O1failure disappear.The
ScopeMapcode in Compose Runtime is a Kotlin value class wrapping an AndroidXMutableScatterMap. A standalone reduced test using a similar value class andMutableScatterMapdoes not reproduce the issue, so the failure appears to require the actual Compose invalidation path rather than the collection/value-class construct alone.Run-Time Log Output and Error Messages
The minimal relevant output from GraalVM CE 25.2.4+7.1 is:
Then, for the same generated
Sample.exe: