Skip to content

[pull] master from ruby:master - #1280

Merged
pull[bot] merged 12 commits into
turkdevops:masterfrom
ruby:master
Aug 5, 2026
Merged

[pull] master from ruby:master#1280
pull[bot] merged 12 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Aug 5, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

nirvdrum and others added 12 commits August 4, 2026 21:34
…H-18108)

`exit_recompile()` decides whether to invalidate a compiled unit by asking whether the interpreter has finished re-profiling the instruction that exited. It used to identify that instruction by reading `ec->cfp->iseq` and `ec->cfp->pc`. But, we already know those values at compile time, so there's no need to go through `ec`. Moreover, the indirection breaks for any exit that doesn't write its ISEQ and PC into the CFP, as is the case for virtual inline frames, where inlined callees have no physical control frame. There, `ec->cfp` is a perfectly valid CFP -- just not the one for the inlined ISEQ that exited -- so the check reads the profile counter of an unrelated instruction, which never reports as complete. The guard then asks for a recompile on every miss and never gets one.
Wiping the body of a hollowed-out object (556296c) is wrong for a string
whose bytes another string reads in place:

    r = Ractor.new { Ractor.receive }
    str = "x" * 100
    str.instance_variable_set(:@Iv, [])   # unshareable, so it is moved
    str.freeze
    dup = str.dup                          # reads str's bytes in place
    r.send(str, move: true)
    dup                                    #=> "\0\0\0..." (was "xxx...")

str_replace_shared_without_enc() shares instead of copying whenever the target's
embedded capacity is too small, and the shared bytes are the source's own slot
when the source is embedded.  The sharer keeps the root alive and reads that slot
for as long as it lives, so the move must leave it as it is.  Skip the wipe for an
embedded shared root; every other case (a private heap buffer, a plain embedded
string) keeps it.

The default GC embeds strings up to a few hundred bytes and mmtk embeds any size,
so this is reachable with an ordinary dup of a frozen string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
We should be able to see through the fields_obj but right now
setivar/getivar disagree on what the return type is
(RubyValue/BasicObject) so it doesn't get optimized. We will fix this in
the next commit.
It's an imemo, so not a BasicObject. This way, also, the return types in
setivar and getivar align and therefore we can optimize this better in
load-store elimination.
Use this for extended fields.
gen_materialize_exit_trampoline clears cfp->jit_return and then calls
rb_zjit_materialize_frames.  CFP_ZJIT_FRAME_P() is exactly
cfp->jit_return != NULL, so by then the exiting frame no longer looks like
a JIT frame and zjit_materialize_frames() skips its whole per-frame body,
including the branch that clears block_code:

    if (jit_frame->materialize_block_code) {
        cfp->block_code = NULL;
    }

Nothing else clears it.  compile_exit_save_state() does not write
block_code, and its comment in zjit/src/backend/lir.rs already states that
this trampoline clears both fields, which it never did.  The exiting frame
therefore keeps the block_code ZJIT wrote for it, and
rb_execution_context_mark() marks cfp->block_code; once that iseq is
collected, marking it is "[BUG] try to mark T_NONE".

An instrumented build counts how often the exiting frame reaches
zjit_materialize_frames() with a non-NULL block_code: 5 times in the first
162 trampoline calls of a single btest process on master, 0 with this
change.

Clearing jit_return after materializing instead would be wrong: skipping
the exiting frame is deliberate, since compile_exit_save_state() has
already written its state and the stack-map restore would overwrite it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The recent change to use `BasicObject.instance_method(:equal?)` broke
the ability to share frozen ERB templates across Ractors because
`UnboundMethod` isn't shareable.

Freezing the `UnboundMethod` _may_ fix the issue (dependong on Ruby
version), but replacing the constant with an inline call to the
`singleton_class` is simpler (and still avoids calling `equal?` on
`@init`).

There have previously been many contributions to make ERB Ractor safe,
but no tests added to ensure it continues to be Ractor safe, so this
commit also adds some regression tests.

ruby/erb@75f1ea059e
Ractor.make_shareable isn't needed if everything is plain `freeze`d

ruby/erb@50f434e7e5
Each arm of a polymorphic dispatch takes a fresh Snapshot so that the
receiver resolves from its refined type instead of the polymorphic
profile. However, the fresh Snapshot had no ProfileOracle entries at
all, so argument-profile-dependent specializations were disabled in
every arm: e.g. Array#[] stayed a CCallVariadic because likely_a
could not see the Fixnum profile of the index.

Copy the profile entries from the original Snapshot to the per-arm
Snapshot, excluding only the receiver's entry (chased through guards),
since resolve_receiver_type prefers profiles over static types.

On optcarrot, this lets the Array arm of the polymorphic `@fetch[addr]`
callsites inline ArrayAref, removing ~3.9M frame-pushing Array#[]
calls per iteration (53% of all not-inlined cfunc calls).
@pull pull Bot locked and limited conversation to collaborators Aug 5, 2026
@pull pull Bot added the ⤵️ pull label Aug 5, 2026
@pull
pull Bot merged commit 022c795 into turkdevops:master Aug 5, 2026
1 of 3 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants