Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@jez
Copy link
Collaborator

@jez jez commented Dec 20, 2025

Motivation

This solves for the case of missing errors on the fast path at the same time as
opening the door to one day expand our "does not take a block" logic to more
than just # typed: strict files, by removing our reliance on
isSyntheticBlockParameter

Test plan

See included automated tests.

jez added 5 commits December 19, 2025 20:07
This suggests an even cooler implementation where we can expand support
for this to non-`# typed: strict` files, but that'll be a more invasive
change so I'm punting it.
@jez jez requested a review from a team as a code owner December 20, 2025 05:24
@jez jez requested review from elliottt and neilparikh and removed request for a team December 20, 2025 05:24
@jez
Copy link
Collaborator Author

jez commented Dec 20, 2025

We have a policy of testing changes to Sorbet against Stripe's codebase before
merging them. I've kicked off a test run for the current PR. When the build
finishes, I'll share with you whether or how it failed. Thanks!

Stripe employees can see the build results here:

https://go/builds/bui_TdZquRh5C5VFvv
https://go/builds/bui_TdZq7hlkJ6XQDK
https://go/builds/bui_TdZqOMCkSZuYMz

Copy link
Collaborator

@elliottt elliottt left a comment

Choose a reason for hiding this comment

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

Overall I think this is a great change! I love that we can control whether or not a block argument is expected without having to have the definition in a typed: strict file now.

The only thing that I think could cause confusion is that the block param is now typed as NilClass in the body of the method. I think it's worth making this change anyway, but I wasn't sure if you had thoughts on how we could avoid the confusion that might arise from error messages mentioning NilClass.

auto blockLoc = args.blockLoc(gs);
if (file.exists() && file.data(gs).strictLevel >= core::StrictLevel::Strict &&
blockParam.isSyntheticBlockParameter() && blockLoc.exists() && !blockLoc.empty()) {
if (file.exists() && blockType.isBottom() && blockLoc.exists() && !blockLoc.empty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I really like that this doesn't rely on the typed sigil of the file now 🎉

extend T::Sig

sig { params(blk: T.noreturn).returns(Integer) }
def takes_no_block(&blk)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does the VM behave differently if we define a block param or if we skip it? It's a little odd that we have to define a block param in order to declare that we don't accept one, and I wonder if there's any potential runtime overhead for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Does the VM behave differently if we define a block param or if we skip it?

Yes, defining and not using a block param slows the program down:

image
❯ hyperfine 'ruby unused_block_param.rb' 'ruby no_block_param.rb'
Benchmark 1: ruby unused_block_param.rb
  Time (mean ± σ):      2.379 s ±  0.043 s    [User: 2.287 s, System: 0.017 s]
  Range (min … max):    2.330 s …  2.457 s    10 runs

Benchmark 2: ruby no_block_param.rb
  Time (mean ± σ):      1.502 s ±  0.037 s    [User: 1.419 s, System: 0.015 s]
  Range (min … max):    1.447 s …  1.591 s    10 runs

Summary
  ruby no_block_param.rb ran
    1.58 ± 0.05 times faster than ruby unused_block_param.rb

unused_block_param.rb

# typed: true

def foo(&blk)
end

i = 0
while i <= 100_000_000
  foo
  i += 1
end

**no_block_param.rb**

```ruby
# typed: true

def foo
end

i = 0
while i <= 100_000_000
  foo
  i += 1
end

It's a little odd that we have to define a block param in order to declare that we don't accept one

You can declare and type this block parameter manually, but the user does not have to. All methods with signatures (including all those in # typed: strict files) whose method definitions do not declare block parameters and thus whose sigs don't provide a type for those block parameters will get an implicit T.noreturn type.

This test just proves that it's possible to write that annotation manually, if you wanted.

I wonder if there's any potential runtime overhead for this.

The thing I wonder is whether it's possible to have runtime overhead in a method that uses the block param.

In all my tests, whether or not you explicitly name the block parameter doesn't matter for performance, as long as you don't do something like blk.call.

If there is a slowdown in this case, that's a case against annotations, because the act of adding an annotation slows things down. This is a separate issue from this PR, but there are two options:

  • Maybe it's sufficient to land the changes in this PR: Allow & to specify anonymous block parameter types #9768 (because you can't materialize a Proc object if the block parameter isn't a syntactically valid variable name.
  • If that's not sufficient, maybe we could extend the spirit of those changes to allow specifying a type for "&": ... in the signature, even if the block parameter was omitted in the method def.

update It looks like there is overhead

image
❯ rg -A 1 def
yield_no_name.rb
3:def foo
4-  yield

named_but_yield.rb
3:def foo(&blk)
4-  yield

blk_call.rb
4:def foo(&blk)
5-  blk.call

def self.implicit_yield
# ^^^^^^^^^^^^^^^^^^^^^^^ error: uses `yield` but does not mention a block parameter
yield
# ^^^^^ error: Method `call` does not exist on `NilClass`
Copy link
Collaborator

Choose a reason for hiding this comment

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

The errors changing to being about NilClass values is a little misleading. Is there any way that we could catch these and give a more specific message?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could, though if you don't pass a block to a method, the block parameter is nil at runtime.

The alternative I can think of seems overkill: we could LoadArg to a special type like Sorbet::Private::Static::NoBlock and attempt to catch uses of that type in dispatchCallSymbol. But it would leak into all calls to isSubType if we don't also guard all of those calls.

I will say that the "does not exist on NilClass" is at least much less confusing than the first version of this that I threw together, which left the type as T.noreturn... the dead code errors were very confusing (and it also caused problems with hover, because it would make code look dead, but the dead code errors would be silent).

I see you approved this, so I'm going to go ahead and land this, but if you have feedback async for how you think we should improve this lmk.

@jez jez merged commit c6519ff into master Dec 22, 2025
22 checks passed
@jez jez deleted the jez-no-return branch December 22, 2025 17:16
neilparikh added a commit that referenced this pull request Dec 22, 2025
neilparikh added a commit that referenced this pull request Dec 23, 2025
* failing test

* Revert "Use `T.noreturn` for "does not take a block" (#9789)"

This reverts commit c6519ff.

* update failing test

* rename test
jez added a commit that referenced this pull request Dec 23, 2025
jez added a commit that referenced this pull request Dec 23, 2025
* Reapply "Use T.noreturn for "does not take a block" (#9789)" (#9800)

This reverts commit 8985e1c.

* Add test showing behavior

* Also check whether it's a subtype of bottom

The `== nilClass` check is redundant with the later `isSubType` of
check, but the `nilClass` check is an integer equality comparison
because ClassType's are inlined types, so we may as well short circuit
after that cheat check.
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.

3 participants