Skip to content

Commit a174c1b

Browse files
author
Nicolas Laurent
committed
[GR-20180] Implement Thread/Exception #backtrace_locations.
PullRequest: truffleruby/1218
2 parents 0c6cfff + 346921d commit a174c1b

21 files changed

+292
-63
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Bug fixes:
5252
* Make `GC.start` work with keyword arguments.
5353
* Fixed `Kernel#clone` for `nil`, `true`, `false`, `Integer`, and `Symbol`.
5454
* Make top-level methods available in `Context#getBindings()` (#1838).
55+
* Made `Kernel#caller_locations` accept a range argument, and return `nil` when appropriate.
5556

5657
Compatibility:
5758

@@ -73,6 +74,7 @@ Compatibility:
7374
* Implemented `rb_io_set_nonblock` (#1741).
7475
* Include the major kernel version in `RUBY_PLATFORM` on macOS like MRI (#1860, @eightbitraptor).
7576
* Implemented `Enumerator::Chain`, `Enumerator#+`, and `Enumerable#chain` (#1859, #1858).
77+
* Implemented `Thread#backtrace_locations` and `Exception#backtrace_locations` (#1556).
7678

7779
Performance:
7880

spec/ruby/core/kernel/caller_locations_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,39 @@
2222
locations.length.should == 1
2323
end
2424

25+
it "can be called with a range" do
26+
locations1 = caller_locations(0)
27+
locations2 = caller_locations(2..4)
28+
locations1[2..4].map(&:to_s).should == locations2.map(&:to_s)
29+
end
30+
31+
it "can be called with a range whose end is negative" do
32+
locations1 = caller_locations(0)
33+
locations2 = caller_locations(2..-1)
34+
locations3 = caller_locations(2..-2)
35+
locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s)
36+
locations1[2..-2].map(&:to_s).should == locations3.map(&:to_s)
37+
end
38+
39+
it "must return nil if omitting more locations than available" do
40+
caller_locations(100).should == nil
41+
caller_locations(100..-1).should == nil
42+
end
43+
44+
it "must return [] if omitting exactly the number of locations available" do
45+
omit = caller_locations(0).length
46+
caller_locations(omit).should == []
47+
end
48+
2549
it 'returns the locations as Thread::Backtrace::Location instances' do
2650
locations = KernelSpecs::CallerLocationsTest.locations
2751

2852
locations.each do |location|
2953
location.kind_of?(Thread::Backtrace::Location).should == true
3054
end
3155
end
56+
57+
it "must return the same locations when called with 1..-1 and when called with no arguments" do
58+
caller_locations.map(&:to_s).should == caller_locations(1..-1).map(&:to_s)
59+
end
3260
end

spec/ruby/core/kernel/warn_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
-> { w.f4(obj, 2) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f2_call_lineno}: warning: to_s called|)
112112
end
113113

114-
it "does not prepend caller information if line number is too big" do
114+
it "does not prepend caller information if the uplevel argument is too large" do
115115
w = KernelSpecs::WarnInNestedCall.new
116116
-> { w.f4("foo", 100) }.should output(nil, "warning: foo\n")
117117
end

spec/ruby/core/thread/backtrace_locations_spec.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,43 @@
1919
locations.each { |loc| loc.should be_an_instance_of(Thread::Backtrace::Location) }
2020
end
2121

22+
it "can be called with a number of locations to omit" do
23+
locations1 = Thread.current.backtrace_locations
24+
locations2 = Thread.current.backtrace_locations(2)
25+
locations1[2..-1].length.should == locations2.length
26+
locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s)
27+
end
28+
29+
it "can be called with a maximum number of locations to return as second parameter" do
30+
locations1 = Thread.current.backtrace_locations
31+
locations2 = Thread.current.backtrace_locations(2, 3)
32+
locations1[2..4].map(&:to_s).should == locations2.map(&:to_s)
33+
end
34+
35+
it "can be called with a range" do
36+
locations1 = Thread.current.backtrace_locations
37+
locations2 = Thread.current.backtrace_locations(2..4)
38+
locations1[2..4].map(&:to_s).should == locations2.map(&:to_s)
39+
end
40+
41+
it "can be called with a range whose end is negative" do
42+
locations1 = Thread.current.backtrace_locations
43+
locations2 = Thread.current.backtrace_locations(2..-1)
44+
locations3 = Thread.current.backtrace_locations(2..-2)
45+
locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s)
46+
locations1[2..-2].map(&:to_s).should == locations3.map(&:to_s)
47+
end
48+
49+
it "returns nil if omitting more locations than available" do
50+
Thread.current.backtrace_locations(100).should == nil
51+
Thread.current.backtrace_locations(100..-1).should == nil
52+
end
53+
54+
it "returns [] if omitting exactly the number of locations available" do
55+
omit = Thread.current.backtrace_locations.length
56+
Thread.current.backtrace_locations(omit).should == []
57+
end
58+
2259
it "without argument is the same as showing all locations with 0..-1" do
2360
Thread.current.backtrace_locations.map(&:to_s).should == Thread.current.backtrace_locations(0..-1).map(&:to_s)
2461
end

spec/tags/core/exception/backtrace_locations_tags.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
fails:Thread#backtrace_locations returns an Array
2-
fails:Thread#backtrace_locations sets each element to a Thread::Backtrace::Location
3-
fails:Thread#backtrace_locations can be called on any Thread
4-
fails:Thread#backtrace_locations without argument is the same as showing all locations with 0..-1
5-
fails:Thread#backtrace_locations the first location reports the call to #backtrace_locations
6-
fails:Thread#backtrace_locations [1..-1] is the same as #caller_locations(0..-1) for Thread.current
1+
fails:Thread#backtrace_locations the first location reports the call to #backtrace_locations

src/main/java/org/truffleruby/core/exception/CoreExceptions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,8 @@ public DynamicObject nameError(String message, Object receiver, String name, Nod
729729
null,
730730
backtrace,
731731
cause,
732+
null,
733+
null,
732734
receiver,
733735
context.getSymbolTable().getSymbol(name)));
734736
}
@@ -744,6 +746,8 @@ public DynamicObject nameErrorFromMethodMissing(DynamicObject formatter, Object
744746
formatter,
745747
backtrace,
746748
cause,
749+
null,
750+
null,
747751
receiver,
748752
context.getSymbolTable().getSymbol(name)));
749753
showExceptionIfDebug(exception, backtrace);
@@ -766,6 +770,8 @@ public DynamicObject noMethodError(String message, Object receiver, String name,
766770
null,
767771
backtrace,
768772
cause,
773+
null,
774+
null,
769775
receiver,
770776
context.getSymbolTable().getSymbol(name),
771777
argsArray));
@@ -784,6 +790,8 @@ public DynamicObject noMethodErrorFromMethodMissing(DynamicObject formatter, Obj
784790
formatter,
785791
backtrace,
786792
cause,
793+
null,
794+
null,
787795
receiver,
788796
context.getSymbolTable().getSymbol(name),
789797
argsArray));
@@ -807,6 +815,8 @@ public DynamicObject noSuperMethodOutsideMethodError(Node currentNode) {
807815
backtrace,
808816
cause,
809817
null,
818+
null,
819+
null,
810820
// FIXME: the name of the method is not known in this case currently
811821
context.getSymbolTable().getSymbol("<unknown>")));
812822
}

src/main/java/org/truffleruby/core/exception/ExceptionLayout.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ Object[] build(
2828
@Nullable Object message,
2929
@Nullable DynamicObject formatter,
3030
@Nullable Backtrace backtrace,
31-
DynamicObject cause);
31+
DynamicObject cause,
32+
@Nullable DynamicObject backtraceStringArray,
33+
@Nullable DynamicObject backtraceLocations);
3234

3335
boolean isException(DynamicObject object);
3436

@@ -48,4 +50,11 @@ Object[] build(
4850

4951
void setCause(DynamicObject object, DynamicObject value);
5052

53+
DynamicObject getBacktraceStringArray(DynamicObject object);
54+
55+
void setBacktraceStringArray(DynamicObject object, DynamicObject value);
56+
57+
DynamicObject getBacktraceLocations(DynamicObject object);
58+
59+
void setBacktraceLocations(DynamicObject object, DynamicObject value);
5160
}

src/main/java/org/truffleruby/core/exception/ExceptionNodes.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public abstract static class AllocateNode extends CoreMethodArrayArgumentsNode {
4444

4545
@Specialization
4646
protected DynamicObject allocateNameError(DynamicObject rubyClass) {
47-
return allocateObjectNode.allocate(rubyClass, Layouts.EXCEPTION.build(nil(), null, null, nil()));
47+
return allocateObjectNode
48+
.allocate(rubyClass, Layouts.EXCEPTION.build(nil(), null, null, nil(), null, null));
4849
}
4950

5051
}
@@ -151,14 +152,14 @@ protected Object backtrace(
151152
if (hasCustomBacktraceProfile.profile(customBacktrace != null)) {
152153
return customBacktrace;
153154
} else if (hasBacktraceProfile.profile(Layouts.EXCEPTION.getBacktrace(exception) != null)) {
154-
final Backtrace backtrace = Layouts.EXCEPTION.getBacktrace(exception);
155-
if (backtrace.getBacktraceStringArray() == null) {
156-
backtrace.setBacktraceStringArray(
157-
getContext().getUserBacktraceFormatter().formatBacktraceAsRubyStringArray(
158-
exception,
159-
Layouts.EXCEPTION.getBacktrace(exception)));
155+
DynamicObject backtraceStringArray = Layouts.EXCEPTION.getBacktraceStringArray(exception);
156+
if (backtraceStringArray == null) {
157+
backtraceStringArray = getContext().getUserBacktraceFormatter().formatBacktraceAsRubyStringArray(
158+
exception,
159+
Layouts.EXCEPTION.getBacktrace(exception));
160+
Layouts.EXCEPTION.setBacktraceStringArray(exception, backtraceStringArray);
160161
}
161-
return backtrace.getBacktraceStringArray();
162+
return backtraceStringArray;
162163
} else {
163164
return nil();
164165
}
@@ -175,6 +176,27 @@ private ReadObjectFieldNode getReadCustomBacktraceNode() {
175176

176177
}
177178

179+
@CoreMethod(names = "backtrace_locations")
180+
public abstract static class BacktraceLocationsNode extends CoreMethodArrayArgumentsNode {
181+
182+
@Specialization
183+
protected Object backtraceLocations(DynamicObject exception,
184+
@Cached("createBinaryProfile()") ConditionProfile hasBacktraceProfile,
185+
@Cached("createBinaryProfile()") ConditionProfile hasLocationsProfile) {
186+
if (hasBacktraceProfile.profile(Layouts.EXCEPTION.getBacktrace(exception) != null)) {
187+
DynamicObject backtraceLocations = Layouts.EXCEPTION.getBacktraceLocations(exception);
188+
if (hasLocationsProfile.profile(backtraceLocations == null)) {
189+
Backtrace backtrace = Layouts.EXCEPTION.getBacktrace(exception);
190+
backtraceLocations = backtrace.getBacktraceLocations(GetBacktraceException.UNLIMITED);
191+
Layouts.EXCEPTION.setBacktraceLocations(exception, backtraceLocations);
192+
}
193+
return backtraceLocations;
194+
} else {
195+
return nil();
196+
}
197+
}
198+
}
199+
178200
@Primitive(name = "exception_backtrace?")
179201
public abstract static class BacktraceQueryPrimitiveNode extends PrimitiveArrayArgumentsNode {
180202

src/main/java/org/truffleruby/core/exception/ExceptionOperations.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public static DynamicObject createRubyException(RubyContext context, DynamicObje
7777
context.getCoreExceptions().showExceptionIfDebug(rubyClass, message, backtrace);
7878
return Layouts.CLASS
7979
.getInstanceFactory(rubyClass)
80-
.newInstance(Layouts.EXCEPTION.build(message, null, backtrace, cause));
80+
.newInstance(Layouts.EXCEPTION.build(message, null, backtrace, cause, null, null));
8181
}
8282

8383
// because the factory is not constant
@@ -88,7 +88,7 @@ public static DynamicObject createSystemCallError(RubyContext context, DynamicOb
8888
final DynamicObject cause = ThreadGetExceptionNode.getLastException(context);
8989
context.getCoreExceptions().showExceptionIfDebug(rubyClass, message, backtrace);
9090
return Layouts.CLASS.getInstanceFactory(rubyClass).newInstance(
91-
Layouts.SYSTEM_CALL_ERROR.build(message, null, backtrace, cause, errno));
91+
Layouts.SYSTEM_CALL_ERROR.build(message, null, backtrace, cause, null, null, errno));
9292
}
9393

9494
public static DynamicObject getFormatter(String name, RubyContext context) {

0 commit comments

Comments
 (0)