Skip to content

Generate compilable code for non-finite floating-point values#36965

Open
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/valuecodegen-nan-infinity
Open

Generate compilable code for non-finite floating-point values#36965
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/valuecodegen-nan-infinity

Conversation

@junhyeong9812

@junhyeong9812 junhyeong9812 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Overview

The AOT ValueCodeGenerator emitted non-compilable Java for non-finite Float and Double values (NaN, positive and negative infinity), so any bean property or constructor argument holding such a value broke compilation of the generated AOT sources. This change emits proper constant field references instead.

Problem

PrimitiveDelegate rendered floats via CodeBlock.of("$LF", value) and doubles via CodeBlock.of("(double) $L", value). Since $L emits the value's toString() verbatim, the non-finite values became invalid source:

value generated compiles?
Float.NaN NaNF no
Float.POSITIVE_INFINITY InfinityF no
Double.NaN (double) NaN no
Double.POSITIVE_INFINITY (double) Infinity no

Fix

Detect NaN with isNaN() (because NaN == NaN is always false) and the infinities with ==, emitting the corresponding constant field references through the $T placeholder (Float.NaN, Double.POSITIVE_INFINITY, and so on), which are valid compile-time constants. Finite values keep their existing handling, so normal numbers are unaffected. Tests covering Float/Double × {NaN, +Infinity, -Infinity} are added to ValueCodeGeneratorTests.

PrimitiveDelegate generated code via "$LF" for Float and "(double) $L"
for Double, which emit the value's toString() verbatim. For NaN and
infinities this produced non-compilable source such as "NaNF" or
"(double) Infinity", causing the generated AOT sources to fail to
compile.

Detect NaN (via isNaN, since NaN is never equal to itself) and the
positive/negative infinities, emitting the corresponding constant
field references (Float.NaN, Double.POSITIVE_INFINITY, etc.) through
the "$T" placeholder. Finite values keep their existing handling.

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 24, 2026
@junhyeong9812 junhyeong9812 force-pushed the fix/valuecodegen-nan-infinity branch from 4bac16c to a30836e Compare June 24, 2026 12:39
@junhyeong9812

Copy link
Copy Markdown
Contributor Author

The Build Pull Request failure appears unrelated to this change. The only failing test is SimpleAsyncTaskExecutorTests.taskTerminationTimeoutWithImmediateCancel() (an AssertionError at SimpleAsyncTaskExecutorTests.java:167), a timing-sensitive concurrency test in core/task, whereas this PR only touches aot/generate/ValueCodeGeneratorDelegates and its test. The added ValueCodeGeneratorTests cases pass, and the failing test passes locally on a clean run.

Could the build be re-run when convenient? Happy to rebase or adjust if anything else is needed.

@junhyeong9812

Copy link
Copy Markdown
Contributor Author

For reference, a closer look at the unrelated CI failure (SimpleAsyncTaskExecutorTests.taskTerminationTimeoutWithImmediateCancel, line 167):

The assertion expects future.get() to throw CancellationException. That only happens if checkCancelled(...) observes cancelled == true. However:

  • cancelled is set by close() on the calling thread (after the termination-timeout wait).
  • checkCancelled(this.future) runs at the very start of the task thread in TaskTrackingRunnable.run(), before the thread adds itself to activeThreads.

So the cancellation path is reached only when close() wins the race and sets cancelled before the freshly started task thread reaches checkCancelled. When the task thread is scheduled quickly (more likely on a loaded CI runner), it passes the checkCancelled gate while cancelled is still false, and the trivial task body (finished is still false at that point) completes normally — so future.get() returns instead of throwing, and the assertion fails.

This looks like a pre-existing timing sensitivity in the test rather than anything related to this change; the test passes locally on a clean run. I could not find an existing tracked issue for it. I have intentionally kept it out of this PR to stay focused, but I am happy to open a separate issue (or look into a fix) if that would be useful.

@bclozel

bclozel commented Jun 24, 2026

Copy link
Copy Markdown
Member

@junhyeong9812 I've started a new run for the workflow. If you've got some spare cycles, we'd love to get a separate contribution to fix that flaky test. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants