Skip to content

Push a range for every end when the begin is a Fixnum - #9833

Merged
headius merged 1 commit into
jruby:masterfrom
sferik:jit-fixnum-range
Oct 20, 2026
Merged

headius merged 1 commit into
jruby:masterfrom
sferik:jit-fixnum-range

Conversation

@sferik

@sferik sferik commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #9823.

JVMVisitor#Range has fast paths for a few literal range shapes. When the begin was a Fixnum, it returned after checking for a Fixnum or nil end, even if the end was neither, so something like (1..2.0) pushed nothing and left the stack one value short. Depending on the surrounding code, that came out as a VerifyError, an ArrayIndexOutOfBoundsException or a NegativeArraySizeException, and the method stayed interpreted.

Now it only returns from the branches that actually push a range, so any other end falls through to the generic path at the bottom, as (1.0..10) already did.

Testing that path in AOT mode turned up a second problem: NormalValueCompiler#pushRange called a RubyRange.newRange(ThreadContext, IRubyObject, IRubyObject) overload that doesn't exist, in both branches, so it would have thrown NoSuchMethodError and ignored exclusive ranges anyway. It now calls newInclusiveRange or newExclusiveRange. The JIT uses IndyValueCompiler for values even with invokedynamic off, so this only affected AOT-compiled code.

The new spec covers Float, Rational and Bignum ends, and inclusive and exclusive ranges. It fails in JIT and AOT mode before this change and passes in all four modes after it. Ranges with a string end, like (1.."3"), still don't compile, but that's #9822, which is a separate fix.

JVMVisitor#Range returned after the Fixnum begin branch even when the
end was neither a Fixnum nor nil, so a range like (1..2.0) pushed
nothing and left the stack one value short. Depending on the code
around it, that showed up as a VerifyError or as an exception from ASM,
and the method stayed interpreted.

Return only from the branches that push something, so other ends fall
through to the generic path. That path's AOT version called a
RubyRange.newRange overload that doesn't exist, and ignored exclusive
ranges, so it now calls newInclusiveRange or newExclusiveRange.

A string end, as in (1.."3"), still doesn't compile, because of
jruby#9822.

Fixes jruby#9823
sferik added a commit to sferik/jruby that referenced this pull request Oct 19, 2026
buildRange turned a string literal endpoint into a FrozenString with
the StrNode's file name, but Node#getFile always returns null. When
both endpoints were literals the file was never used, but a range like
"a"..x or ("a"..) visits the FrozenString on its own, which passes the
file to invokedynamic as a bootstrap argument, and ASM rejects null.
Those methods couldn't be JIT-compiled.

Use scope.getFile(), as every other FrozenString in the builder does.

Endless and beginless string ranges also go through the AOT fallback in
NormalValueCompiler#pushRange, which called a RubyRange.newRange
overload that doesn't exist. This includes the same fix for that as
jruby#9833, so the two merge cleanly in
either order.

Fixes jruby#9822

@headius headius left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah those pesky peephole optimizations. Simple fix, looks good.

@headius headius added this to the JRuby 10.1.3.0 milestone Oct 20, 2026
@headius
headius merged commit b05985a into jruby:master Oct 20, 2026
135 checks passed
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.

JIT emits invalid bytecode for a range literal with an Integer begin and a non-Integer literal end, such as (1..10.0)

2 participants