Project

General

Profile

Actions

Bug #17105

closed

A single `return` can return to two different places in a proc inside a lambda inside a method

Added by Eregon (Benoit Daloze) about 4 years ago. Updated over 3 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:99499]

Description

A single return in the source code might return to 2 different lexical places.
That seems wrong to me, as AFAIK all other control flow language constructs always jump to a single place.

def m(call_proc)
  r = -> {
    # This single return in the source might exit the lambda or the method!
    proc = Proc.new { return :return }

    if call_proc
      proc.call
      :after_in_lambda
    else
      proc
    end
  }.call # returns here if call_proc

  if call_proc
    [:after_in_method, r]
  else
    r.call
    :never_reached
  end
end


p m(true)  # => [:after_in_method, :return]
p m(false) # :return

We're trying to figure out the semantics of return inside a proc in
https://github.com/oracle/truffleruby/issues/1488#issuecomment-669185675
and this behavior doesn't seem to make much sense.

@headius (Charles Nutter) also seems to agree:

I would consider that behavior to be incorrect; once the proc has escaped from the lambda, its return target is no longer valid. It should not return to a different place.
https://github.com/jruby/jruby/issues/6350#issuecomment-669603740

So:

  • is this behavior intentional? or is it a bug?
  • what are actually the semantics of return inside a proc?

The semantics seem incredibly complicated to a point developers have no idea where return actually goes.
Also it must get even more complicated if one defines a lambda method as the block in lambda { return } is then non-deterministically a proc or lambda.

Updated by Eregon (Benoit Daloze) about 4 years ago

I should also note some of these semantics might significantly harm the performance of Ruby.
CRuby seems to walk the stack on every return.
On others VMs there need to be some extra logic to find if the frame to return to is still on the stack.
It's already quite complicated but then if return can go to two places, it becomes a huge mess.

Updated by Hanmac (Hans Mackowiak) about 4 years ago

i think this is by design:

https://www.rubyguides.com/2016/02/ruby-procs-and-lambdas/

A lambda will return normally, like a regular method.
But a proc will try to return from the current context.

Procs return from the current method, while lambdas return from the lambda itself.

Updated by chrisseaton (Chris Seaton) about 4 years ago

Hans I don't think anyone is debating the basic idea of what return in a proc or lambda does - I think we're talking about the edge-case for a proc in a return in the example above, which isn't explained by the text you have.

Updated by Dan0042 (Daniel DeLorme) about 4 years ago

I think the behavior makes sense to some extent, because the proc is within 2 nested contexts. Since the proc is within the lambda context, calling it in the lambda returns from the lambda. And since the proc is also within the method context, calling it in the method returns from the method.

The call_proc branching logic makes this look more complicated than it really is, but if you separate the logic I feel the behavior is rather reasonable. What do you think should be the behavior of m2 below?

def m1
  r = -> {
    proc = Proc.new{ return :return }
    proc.call #return from lambda
    :after_in_lambda
  }.call
 
  [:after_in_method, r]
end

def m2
  r = -> {
    proc = Proc.new { return :return }
  }.call

  r.call #return from method
  :never_reached
end

p m1 #=> [:after_in_method, :return]
p m2 #=> :return

Updated by Eregon (Benoit Daloze) about 4 years ago

IMHO it should be a LocalJumpError. The Proc should return to the lambda, that's syntactically the closest scope it should return to.
Since it's not possible to return to it (the lambda is no longer on stack), it should be a LocalJumpError.

Updated by shyouhei (Shyouhei Urabe) about 4 years ago

+1 to @Eregon (Benoit Daloze) ’s interpretation. Current behaviour is at least very cryptic.

Updated by headius (Charles Nutter) about 4 years ago

Just to be clear I am +1 on single return target, as described here: https://github.com/jruby/jruby/issues/6350#issuecomment-669603740

In addition to the confusing (and possibly inefficient) behavior that results from having two possible return targets, there's also a bug potential here if someone "accidentally" allows a proc containing a return to escape from its lambda container. Rather than returning from the lambda as it should have done, it will now return from the next "returnable" scope, and likely interrupt execution in an unexpected way.

I would challenge anyone to explain why the current behavior should exist, since I can't think of a single valid use case. If there's no use case for a confusing "feature", we should remove it.

Updated by matz (Yukihiro Matsumoto) about 4 years ago

It is intentional since 1.6.0. But I am OK with making m2 raise LocalJumpError.
Ask @ko1 (Koichi Sasada) about migration.

Matz.

Actions #10

Updated by ko1 (Koichi Sasada) over 3 years ago

  • Status changed from Open to Closed

Applied in changeset git|ecfa8dcdbaf60cbe878389439de9ac94bc82e034.


fix return from orphan Proc in lambda

A "return" statement in a Proc in a lambda like:
lambda{ proc{ return }.call }
should return outer lambda block. However, the inner Proc can become
orphan Proc from the lambda block. This "return" escape outer-scope
like method, but this behavior was decieded as a bug.
[Bug #17105]

This patch raises LocalJumpError by checking the proc is orphan or
not from lambda blocks before escaping by "return".

Most of tests are written by Jeremy Evans
https://github.com/ruby/ruby/pull/4223

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0