Skip to content

Commit 55c2823

Browse files
committed
Add Thread.each_caller_location.
1 parent 38b5987 commit 55c2823

File tree

8 files changed

+130
-4
lines changed

8 files changed

+130
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Compatibility:
2424
* Add implementations of `rb_proc_call_with_block`, `rb_proc_call_kw`, `rb_proc_call_with_block_kw` and `rb_funcall_with_block_kw` (#3068, @andrykonchin).
2525
* Add optional `timeout` argument to `Thread::Queue#pop` (#3039, @itarato).
2626
* Add optional `timeout` argument to `Thread::SizedsQueue#pop` (#3039, @itarato).
27+
* Add `Thread.each_caller_location` (#3039, @itarato).
2728

2829
Performance:
2930

doc/user/compatibility.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ It is not recommended to use exceptions for control flow on any implementation o
167167

168168
To help alleviate this problem, backtraces are automatically disabled in cases where we can detect that they will not be used.
169169

170+
### Caller locations
171+
172+
Using `Kernel#caller_locations` or `Thread.each_caller_location` might contain engine specific location objects and/or
173+
paths. This is expected and should be filtered in application code where necessary.
174+
175+
The enumerator returned by `Thread.to_enum(:each_caller_location)` is not supporting iteration with `.next`. In CRuby
176+
this raises a `StopIteration`, while in TruffleRuby it iterates on an undetermined (related to where and how `.next` is
177+
called) call stack. It is not recommended to use this in any circumstance (neither CRuby nor TruffleRuby).
178+
170179
## C Extension Compatibility
171180

172181
### Identifiers may be macros or functions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require_relative '../../spec_helper'
2+
3+
describe "Thread.each_caller_location" do
4+
ruby_version_is "3.2" do
5+
it "iterates through the current execution stack and matches caller_locations content and type" do
6+
ScratchPad.record []
7+
Thread.each_caller_location { |l| ScratchPad << l; }
8+
9+
ScratchPad.recorded.map(&:to_s).should == caller_locations.map(&:to_s)
10+
ScratchPad.recorded[0].should be_kind_of(Thread::Backtrace::Location)
11+
end
12+
13+
it "returns subset of 'Thread.to_enum(:each_caller_location)' locations" do
14+
ar = []
15+
ecl = Thread.each_caller_location { |x| ar << x }
16+
17+
(ar.map(&:to_s) - Thread.to_enum(:each_caller_location).to_a.map(&:to_s)).should.empty?
18+
end
19+
20+
it "stops the backtrace iteration if 'break' occurs" do
21+
i = 0
22+
ar = []
23+
ecl = Thread.each_caller_location do |x|
24+
ar << x
25+
i += 1
26+
break x if i == 2
27+
end
28+
29+
ar.map(&:to_s).should == caller_locations(1, 2).map(&:to_s)
30+
ecl.should be_kind_of(Thread::Backtrace::Location)
31+
end
32+
33+
it "returns nil" do
34+
Thread.each_caller_location {}.should == nil
35+
end
36+
37+
it "raises LocalJumpError when called without a block" do
38+
-> {
39+
Thread.each_caller_location
40+
}.should raise_error(LocalJumpError, "no block given")
41+
end
42+
43+
it "doesn't accept positional and keyword arguments" do
44+
-> {
45+
Thread.each_caller_location(12, foo: 10) {}
46+
}.should raise_error(ArgumentError, "wrong number of arguments (given 2, expected 0)")
47+
end
48+
end
49+
end

spec/truffleruby.next-specs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ spec/ruby/core/queue/shift_spec.rb
2929
spec/ruby/core/sizedqueue/deq_spec.rb
3030
spec/ruby/core/sizedqueue/pop_spec.rb
3131
spec/ruby/core/sizedqueue/shift_spec.rb
32+
33+
spec/ruby/core/thread/each_caller_location_spec.rb

src/main/java/org/truffleruby/core/thread/ThreadBacktraceLocationNodes.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ private static SourceSection getAvailableSourceSection(RubyContext context,
3838
final Backtrace backtrace = threadBacktraceLocation.backtrace;
3939
final int activationIndex = threadBacktraceLocation.activationIndex;
4040

41-
return context
42-
.getUserBacktraceFormatter()
43-
.nextAvailableSourceSection(backtrace.getStackTrace(), activationIndex);
41+
return BacktraceFormatter.nextAvailableSourceSection(backtrace.getStackTrace(), activationIndex);
4442
}
4543

4644
@CoreMethod(names = "absolute_path")

src/main/java/org/truffleruby/core/thread/ThreadNodes.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@
4444
import java.util.List;
4545
import java.util.concurrent.TimeUnit;
4646

47+
import com.oracle.truffle.api.RootCallTarget;
4748
import com.oracle.truffle.api.TruffleContext;
4849
import com.oracle.truffle.api.TruffleSafepoint;
4950
import com.oracle.truffle.api.TruffleSafepoint.Interrupter;
51+
import com.oracle.truffle.api.TruffleStackTraceElement;
52+
import com.oracle.truffle.api.frame.FrameInstance;
5053
import com.oracle.truffle.api.frame.VirtualFrame;
5154
import com.oracle.truffle.api.profiles.InlinedBranchProfile;
5255
import com.oracle.truffle.api.profiles.InlinedConditionProfile;
@@ -93,6 +96,7 @@
9396
import org.truffleruby.language.arguments.ArgumentsDescriptor;
9497
import org.truffleruby.language.arguments.RubyArguments;
9598
import org.truffleruby.language.backtrace.Backtrace;
99+
import org.truffleruby.language.backtrace.BacktraceFormatter;
96100
import org.truffleruby.language.control.KillException;
97101
import org.truffleruby.language.control.RaiseException;
98102
import org.truffleruby.language.objects.AllocationTracing;
@@ -1035,4 +1039,59 @@ protected Object runBlockingSystemCall(Object executable, RubyArray argsArray,
10351039
return foreignToRubyNode.executeConvert(result);
10361040
}
10371041
}
1042+
1043+
@CoreMethod(names = "each_caller_location", needsBlock = true, onSingleton = true)
1044+
public abstract static class EachCallerLocationNode extends CoreMethodArrayArgumentsNode {
1045+
1046+
private static final Object STOP_ITERATE = new Object();
1047+
1048+
// Skip the block of `Thread#each_caller_location` + its internal iteration.
1049+
private static final int SKIP = 2;
1050+
1051+
@Child private CallBlockNode yieldNode = CallBlockNode.create();
1052+
1053+
@Specialization
1054+
protected Object eachCallerLocation(VirtualFrame frame, RubyProc block) {
1055+
final List<TruffleStackTraceElement> stackTraceElements = new ArrayList<>();
1056+
1057+
getContext().getCallStack().iterateFrameBindings(SKIP, frameInstance -> {
1058+
final Node location = frameInstance.getCallNode();
1059+
1060+
final RootCallTarget rootCallTarget = (RootCallTarget) frameInstance.getCallTarget();
1061+
final TruffleStackTraceElement stackTraceElement = TruffleStackTraceElement.create(
1062+
location,
1063+
rootCallTarget,
1064+
frameInstance.getFrame(FrameInstance.FrameAccess.READ_ONLY));
1065+
stackTraceElements.add(stackTraceElement);
1066+
1067+
final TruffleStackTraceElement[] finalStackTraceElements = stackTraceElements
1068+
.toArray(TruffleStackTraceElement[]::new);
1069+
final boolean readyToYield = BacktraceFormatter.nextAvailableSourceSection(finalStackTraceElements,
1070+
0) != null;
1071+
1072+
if (readyToYield) {
1073+
for (int i = 0; i < finalStackTraceElements.length; i++) {
1074+
final Backtrace backtrace = new Backtrace(location, 0, finalStackTraceElements);
1075+
RubyBacktraceLocation rubyBacktraceLocation = new RubyBacktraceLocation(
1076+
getContext().getCoreLibrary().threadBacktraceLocationClass,
1077+
getLanguage().threadBacktraceLocationShape,
1078+
backtrace,
1079+
i);
1080+
1081+
yieldNode.yield(block, rubyBacktraceLocation);
1082+
}
1083+
stackTraceElements.clear();
1084+
}
1085+
1086+
return null;
1087+
});
1088+
1089+
return nil;
1090+
}
1091+
1092+
@Specialization
1093+
protected Object eachCallerLocation(VirtualFrame frame, Nil block) {
1094+
throw new RaiseException(getContext(), coreExceptions().localJumpError("no block given", this));
1095+
}
1096+
}
10381097
}

src/main/java/org/truffleruby/language/backtrace/Backtrace.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ public Backtrace(Node location, int omitted, Throwable javaThrowable) {
8787
this.javaThrowable = javaThrowable;
8888
}
8989

90+
/** For manually crafted backtraces. */
91+
public Backtrace(Node location, int omitted, TruffleStackTraceElement[] stackTraceElements) {
92+
this.location = location;
93+
this.omitted = omitted;
94+
this.javaThrowable = null;
95+
this.stackTrace = stackTraceElements;
96+
}
97+
9098
/** Creates a backtrace for the given foreign exception, setting the {@link #getLocation() location} accordingly,
9199
* and computing the activations eagerly (since the exception itself is not retained). */
92100
public Backtrace(AbstractTruffleException exception) {

src/main/java/org/truffleruby/language/backtrace/BacktraceFormatter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ private String formatException(RubyException exception) {
337337

338338
/** This logic should be kept in sync with
339339
* {@link org.truffleruby.debug.TruffleDebugNodes.IterateFrameBindingsNode} */
340-
public SourceSection nextAvailableSourceSection(TruffleStackTraceElement[] stackTrace, int n) {
340+
public static SourceSection nextAvailableSourceSection(TruffleStackTraceElement[] stackTrace, int n) {
341341
while (n < stackTrace.length) {
342342
final Node callNode = stackTrace[n].getLocation();
343343

0 commit comments

Comments
 (0)