Skip to content

YJIT: Fix defined?(yield) and block_given? at top level#14208

Merged
XrXr merged 2 commits intoruby:masterfrom
Shopify:yjit-top-level-yield
Aug 14, 2025
Merged

YJIT: Fix defined?(yield) and block_given? at top level#14208
XrXr merged 2 commits intoruby:masterfrom
Shopify:yjit-top-level-yield

Conversation

@XrXr
Copy link
Copy Markdown
Member

@XrXr XrXr commented Aug 13, 2025

  • File backport:

    $ ruby --yjit-call-threshold=1 -ve 'p block_given?'
    ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin24]
    true
    

Previously, YJIT returned truthy for the block given query at the top
level. That's incorrect because the top level script never receives a
block, and yield is a syntax error there.

Inside methods, the number of hops to get from iseq to
iseq->body->local_iseq is the same as the number of
VM_ENV_PREV_EP(ep) hops to get to an environment with
VM_ENV_FLAG_LOCAL. YJIT and the interpreter both rely on this as can
be seen in get_lvar_level(). However, this identity does not hold for
the top level frame because of vm_set_eval_stack(), which sets up
TOPLEVEL_BINDING.

Since only methods can take a block that yield goes to, have ISEQs
that are the child of a non-method ISEQ return falsy for the block given
query. This fixes the issue for the top level script and is an
optimization for non-method contexts such as inside ISEQ_TYPE_CLASS.


I think this also fixes the issue in #13454. There's no need for a runtime check.

Previously, YJIT returned truthy for the block given query at the top
level. That's incorrect because the top level script never receives a
block, and `yield` is a syntax error there.

Inside methods, the number of hops to get from `iseq` to
`iseq->body->local_iseq` is the same as the number of
`VM_ENV_PREV_EP(ep)` hops to get to an environment with
`VM_ENV_FLAG_LOCAL`. YJIT and the interpreter both rely on this as can
be seen in get_lvar_level(). However, this identity does not hold for
the top level frame because of vm_set_eval_stack(), which sets up
`TOPLEVEL_BINDING`.

Since only methods can take a block that `yield` goes to, have ISEQs
that are the child of a non-method ISEQ return falsy for the block given
query. This fixes the issue for the top level script and is an
optimization for non-method contexts such as inside `ISEQ_TYPE_CLASS`.
@matzbot matzbot requested a review from a team August 13, 2025 18:03
Comment thread yjit/src/codegen.rs
Comment on lines +6660 to +6661
// the current iseq or a parent. Only the "method" iseq type can be passed a
// block handler. (e.g. `yield` in the top level script is a syntax error.)
Copy link
Copy Markdown
Member

@k0kubun k0kubun Aug 13, 2025

Choose a reason for hiding this comment

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

I was kind of confused about ISEQ_TYPE_BLOCK since you can pass a block to a block, but I guess we should still return false because the block_given? method refers to the local environment (LEP) that a block would inherit from a frame of non-ISEQ_TYPE_BLOCK ISEQ.

I'm also unsure what ISEQ_TYPE_MAIN (when there's already ISEQ_TYPE_TOP) and ISEQ_TYPE_PLAIN are, but the assumption probably holds for the rest of the types.

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.

It might be useful to have an assertion for debug builds in either the original C function or generated code to improve our confidence, but compatibility-wise it seems optional since it's passing tests anyway.

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.

I guess we should still return false because the block_given? method refers to the local environment (LEP) that a block would inherit from a frame of non-ISEQ_TYPE_BLOCK ISEQ.

Well, but then the LEP frame could indeed be ISEQ_TYPE_METHOD.

Does it support this kind of situation? (Is the { block_given? } ISEQ not a ISEQ_TYPE_BLOCK?)

irb(main)[01:0]> def foo = self.then { block_given? }
=> :foo
irb(main)[02:0]> foo
=> false
irb(main)[03:0]> foo {}
=> true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Does it support this kind of situation? (Is the { block_given? } ISEQ not a ISEQ_TYPE_BLOCK?)

Yes it works; lots of test code call block_given? from within a block. The block ISEQ in this example is indeed ISEQ_TYPE_BLOCK, but the ISEQ type check happens on block_iseq->body->local_iseq, not on block_iseq, and block_iseq->body->local_iseq->type == ISEQ_TYPE_METHOD.

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.

Sorry, I missed the rb_get_iseq_body_local_iseq portion. Thanks for the explanation!

Copy link
Copy Markdown
Member

@k0kubun k0kubun left a comment

Choose a reason for hiding this comment

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

Thanks for the patch. If the same codegen makes #14208 work, that'd be amazing.

@XrXr XrXr enabled auto-merge (rebase) August 14, 2025 16:03
@XrXr XrXr merged commit 38558dd into ruby:master Aug 14, 2025
91 of 95 checks passed
@XrXr XrXr deleted the yjit-top-level-yield branch August 14, 2025 21:01
tagomoris pushed a commit to tagomoris/ruby that referenced this pull request Sep 20, 2025
* Originally, k0kubun added a change to respect namespace in gen_block_given
  ruby@d129669
* Just after the change, XrXr proposes a change on master and he says it can fix the problem
  around block_given?, and it has been merged into the master
  ruby#14208
* tagomoris respect the commit on master then will see if it works
tagomoris pushed a commit to tagomoris/ruby that referenced this pull request Sep 28, 2025
* Originally, k0kubun added a change to respect namespace in gen_block_given
  ruby@d129669
* Just after the change, XrXr proposes a change on master and he says it can fix the problem
  around block_given?, and it has been merged into the master
  ruby#14208
* tagomoris respect the commit on master then will see if it works
tagomoris pushed a commit to tagomoris/ruby that referenced this pull request Sep 28, 2025
* Originally, k0kubun added a change to respect namespace in gen_block_given
  ruby@d129669
* Just after the change, XrXr proposes a change on master and he says it can fix the problem
  around block_given?, and it has been merged into the master
  ruby#14208
* tagomoris respect the commit on master then will see if it works
tagomoris pushed a commit that referenced this pull request Sep 28, 2025
* Originally, k0kubun added a change to respect namespace in gen_block_given
  d129669
* Just after the change, XrXr proposes a change on master and he says it can fix the problem
  around block_given?, and it has been merged into the master
  #14208
* tagomoris respect the commit on master then will see if it works
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.

2 participants