After a tailcall, the "return" tracepoint event is only fired once. Normally, after a call at the end of a method, the return event is fired twice, once for the callee returning and once for the caller returning.
The following script outputs
:call
:call
:call
:return
method_source=<<-RB
def toy(n)
return if n == 2
toy(n+1)
end
RBiseq=RubyVM::InstructionSequence.compile(method_source,tailcall_optimization: true)#puts iseq.disasmiseq.evaltrace=TracePoint.new(:call,:return)do|tp|ptp.eventendtrace.enabletoy(0)
The "return" event behaves more like a "stack frame pop" event currently. I don't think it's feasible/desirable to have the same behavior when TCO is applied, but it would be nice if there was some way for the tracepoint to know a tail call is going to happen.
I'm raising this issue because the popular debugger "byebug" relies on these events to track execution in various stack frames. https://github.com/deivid-rodriguez/byebug/issues/481
Forwardable explicitly uses TCO which triggers this issue.
The main concern I have with this approach is that there is no reasonable value we can make Tracepoint#return_value return when we trigger it due to a tailcall:
def bar
:"bar's return value"
end
iseq = RubyVM::InstructionSequence.new("def foo; bar; end", __FILE__, __FILE__, __LINE__, tailcall_optimization: true)
iseq.eval
TracePoint.new(:return) {|tp|
p tp
p tp.return_value
}.enable{
foo
}
#<TracePoint:return `foo'@./test.rb:5>
main
#<TracePoint:return `bar'@./test.rb:3>
:"bar's return value"
Clearly, foo doesn't return main; there is no meaningful value we can put for #return_value since semantically, foo hasn't returned yet.
With this patch applied, #return_value is not reliable anymore for return events.
I made #15313 because I felt it's the most backward compatible approach.
If we return nil for #return_value, users can't tell whether the nil is from the method actually returning nil, or a nil from tailcall.
I think the return event should be reserved for only real returns, because of its name.
I don't think this is a huge issue but I'm uncomfortable with ignoring it.
I personally don't see an issue with this spec, besides that it would introduce a breaking change one way or the other. I think it would fix the next command in byebug, though. (it would have to rescue when it calls #return_value, though)
I've asked David Rodríguez, the creator of Byebug to join the discussion on Github. Hopefully we get to hear from him.
Cancel tailcall opt when return event is specified.
I think anyone who goes through all the trouble to enable tailcall elimination right now relies on it to avoid stack overflow.
Why else would someone use it? It doesn't make your program faster, it's cumbersome to enable, and it erases part of your stack trace which makes debugging harder.
Making this change seems to me replacing a problem with a different one. It is a problem that affects way less people admittedly, but I feel that it would be punishing functional programmers for the benefit of everyone else.
If we just want to fix the problem most people have today, i.e., byebug's next command's breakage, making Forwardable not use TCO is good enough. It will still be broken for tailcall optimized code, but at least most people won't run into this problem.
That might be the most pragmatic "fix", and it's a simple one. With it, we can keep ignoring this problem and wait till someone complains.
The original proposal of extending the API to be able to distinguish between regular method calls and tail calls sounds good to me in principle and looks like it would solve the problem in the general case. It would be a niche feature, but tailcall optimization is a niche feature too, right? It just happens to be used in a standard lib method, that's why it's biting byebug.
Regarding koichi's first proposal, it's not bad either. The return_value method in return events is only marginally used in byebug, so it's not a big deal to lose it on these egde cases.
Regarding koichi's last proposal.. Again I agree with alanwu. Couldn't using the tracepoint API over programs relying on tailcall optimization actually break those programs?