Skip to content

Commit a2b769d

Browse files
author
Nicolas Laurent
committed
comment & rearrange Backtrace.java + remove stopgap solution for nil vs [] backtrace locations
1 parent 9b8b857 commit a2b769d

File tree

1 file changed

+185
-102
lines changed

1 file changed

+185
-102
lines changed

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

Lines changed: 185 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -31,197 +31,280 @@
3131
import com.oracle.truffle.api.object.DynamicObject;
3232
import com.oracle.truffle.api.source.SourceSection;
3333

34+
/**
35+
* Represents a backtrace: a list of activations (~ call sites).
36+
*
37+
* <p>A backtrace is always constructed from a Java throwable, but does not always correspond to a
38+
* Ruby exception (e.g. {@code Kernel.caller_locations}). Whenever constructing a backtrace from a
39+
* Ruby exception, it will be encapsulated in a Java throwable ({@link RaiseException}).
40+
*
41+
* <p>Whenever a backtrace is associated with a Ruby exception, there is a 1-1-1 match between
42+
* the backtrace, the Ruby exception (which has a backtrace field) and the
43+
* {@link #getRaiseException() raiseException} stored in the backtrace (which encapsulates the
44+
* Ruby exception).
45+
*
46+
* <p>NOTE(norswap): At least, that's how it should work, but there are cases where a Ruby
47+
* exception's backtrace may not link back to the exception (e.g. in {@code
48+
* TranslateExceptionNode#translateThrowable}).
49+
*
50+
* <p>Note in passing that not all Ruby exceptions have an associated backtrace (simply creating an
51+
* exception via {@code Exception.new} does not fill its backtrace), nor does the associated
52+
* backtrace necessarily match the result of {@code Exception#backtrace} in Ruby, because the user
53+
* may have set a custom backtrace (an array of strings).
54+
*
55+
* <p>In general, there isn't any guarantee that the getters will return non-null values, excepted
56+
* {@link #getActivations(), {@link #getActivations(Throwable)} and {@link #getBacktraceLocations(int)}.
57+
*
58+
* <p>NOTE(norswap): And this is somewhat unfortunate, as it's difficult to track the assumptions
59+
* on the backtrace object and generally require being very defensive depending on the information
60+
* available - but fixing this would require a dangerous refactoring for little benefit.
61+
*
62+
* <p>Also note that the activations are recorded lazily when one of the aforementionned methods is
63+
* called, excepted when specified otherwise in the constructor. The activations will match the
64+
* state of the Truffle call stack whenever the activations are recorded (so, during the constructor
65+
* call or first method call).
66+
*/
3467
public class Backtrace {
3568

69+
// See accessors for info on most undocumented fields.
70+
3671
private final Node location;
37-
/** Only set for SyntaxError, where getLocation() does not represent where the error occurred. */
3872
private final SourceSection sourceLocation;
39-
private RaiseException raiseException;
40-
private Activation[] activations;
4173
private final int omitted;
74+
private RaiseException raiseException;
4275
private final Throwable javaThrowable;
76+
private Activation[] activations;
77+
78+
/** How many activations would there be if omitted was 0? */
79+
private int totalUnderlyingActivations;
4380

81+
// region Constructors
82+
83+
/**
84+
* Fully explicit constructor.
85+
*/
4486
public Backtrace(Node location, SourceSection sourceLocation, int omitted, Throwable javaThrowable) {
4587
this.location = location;
4688
this.sourceLocation = sourceLocation;
4789
this.omitted = omitted;
4890
this.javaThrowable = javaThrowable;
4991
}
5092

93+
/**
94+
* Creates a backtrace for the given Truffle exception, setting the
95+
* {@link #getLocation() location} and {@link #getSourceLocation() source location} accordingly,
96+
* and computing the activations eagerly (since the exception itself is not retained).
97+
*
98+
* <p>This is not/should not be used for constructing the backtrace associated with Ruby
99+
* exceptions.
100+
*/
51101
public Backtrace(TruffleException exception) {
52102
this.location = exception.getLocation();
53103
this.sourceLocation = exception.getSourceLocation();
54104
this.omitted = 0;
55105
this.javaThrowable = null;
56-
57106
this.activations = getActivations((Throwable) exception);
58107
}
59108

109+
/**
110+
* Creates a backtrace for the given throwable, in which only the activations and the backtrace
111+
* locations may be retrieved. The activations are computed eagerly, since the exception itself
112+
* is not retained.
113+
*/
60114
public Backtrace(Throwable exception) {
61115
this.location = null;
62116
this.sourceLocation = null;
63117
this.omitted = 0;
64118
this.javaThrowable = null;
65-
66119
this.activations = getActivations(exception);
67120
}
68121

69-
public Backtrace copy(RubyContext context, DynamicObject exception) {
70-
Backtrace copy = new Backtrace(location, sourceLocation, omitted, javaThrowable);
71-
// A Backtrace is 1-1-1 with a RaiseException and a Ruby exception
72-
RaiseException newRaiseException = new RaiseException(
73-
context,
74-
exception,
75-
this.raiseException.isInternalError());
76-
// Copy the TruffleStackTrace
77-
TruffleStackTrace.fillIn(this.raiseException);
78-
assert this.raiseException.getCause() != null;
79-
newRaiseException.initCause(this.raiseException.getCause());
80-
// Another way would be to copy the activations (copy.activations = getActivations()), but
81-
// then the TruffleStrackTrace would be inconsistent.
82-
copy.setRaiseException(newRaiseException);
83-
return copy;
84-
}
122+
// endregion
123+
// region Accessors
85124

125+
/**
126+
* AST node that caused the associated exception, if the info is available, or null.
127+
*/
86128
public Node getLocation() {
87129
return location;
88130
}
89131

132+
/**
133+
* Only set for {@code SyntaxError}, where it represents where the error occurred
134+
* (while {@link #getLocation()} does not).
135+
*/
90136
public SourceSection getSourceLocation() {
91137
return sourceLocation;
92138
}
93139

140+
/**
141+
* Returns the wrapper for the Ruby exception associated with this backtrace, if any, and
142+
* null otherwise.
143+
*/
94144
public RaiseException getRaiseException() {
95145
return raiseException;
96146
}
97147

148+
/**
149+
* Sets the wrapper for the Ruby exception associated with this backtrace.
150+
*
151+
* <p>Do not set the raise exception twice on the same backtrace!
152+
*/
98153
public void setRaiseException(RaiseException raiseException) {
99154
assert this.raiseException == null : "the RaiseException of a Backtrace must not be set again, otherwise the original backtrace is lost";
100155
this.raiseException = raiseException;
101156
}
102157

103-
public Activation[] getActivations() {
104-
return getActivations(this.raiseException);
158+
/**
159+
* Returns the number of activations to omit from the top (= most recently called) of the
160+
* activation stack.
161+
*/
162+
public int getOmitted() {
163+
return omitted;
164+
}
165+
166+
/**
167+
* Returns the Java exception the associated Ruby exception was translated from, if any.
168+
* (This is not the same as {@link #getRaiseException() the raise exception} which is simply
169+
* a wrapper around the Ruby exception.)
170+
*/
171+
public Throwable getJavaThrowable() {
172+
return javaThrowable;
173+
}
174+
175+
// endregion
176+
177+
/**
178+
* Used to copy the backtrace when copying {@code exception}.
179+
*/
180+
public Backtrace copy(RubyContext context, DynamicObject exception) {
181+
Backtrace copy = new Backtrace(location, sourceLocation, omitted, javaThrowable);
182+
// A Backtrace is 1-1-1 with a RaiseException and a Ruby exception.
183+
RaiseException newRaiseException = new RaiseException(
184+
context,
185+
exception,
186+
this.raiseException.isInternalError());
187+
// Copy the TruffleStackTrace
188+
//noinspection ThrowableNotThrown
189+
TruffleStackTrace.fillIn(this.raiseException);
190+
assert this.raiseException.getCause() != null;
191+
newRaiseException.initCause(this.raiseException.getCause());
192+
// Another way would be to copy the activations (copy.activations = getActivations()), but
193+
// then the TruffleStrackTrace would be inconsistent.
194+
copy.setRaiseException(newRaiseException);
195+
return copy;
105196
}
106197

107198
@TruffleBoundary
108199
public Activation[] getActivations(Throwable truffleException) {
109-
if (this.activations == null) {
110-
if (truffleException == null) {
111-
truffleException = new GetBacktraceException(location, GetBacktraceException.UNLIMITED);
112-
}
200+
if (this.activations != null) {
201+
return this.activations;
202+
}
203+
204+
if (truffleException == null) {
205+
truffleException = new GetBacktraceException(location, GetBacktraceException.UNLIMITED);
206+
}
207+
208+
// The stacktrace is computed here if it was not already computed and stored in the
209+
// TruffleException with TruffleStackTraceElement.fillIn().
210+
final List<TruffleStackTraceElement> stackTrace = TruffleStackTrace.getStackTrace(truffleException);
211+
assert stackTrace != null;
113212

114-
// The stacktrace is computed here if it was not already computed and stored in the
115-
// TruffleException with TruffleStackTraceElement.fillIn().
116-
final List<TruffleStackTraceElement> stackTrace = TruffleStackTrace.getStackTrace(truffleException);
213+
final List<Activation> activations = new ArrayList<>();
214+
final RubyContext context = RubyLanguage.getCurrentContext();
215+
final CallStackManager callStackManager = context.getCallStack();
117216

118-
final List<Activation> activations = new ArrayList<>();
119-
final RubyContext context = RubyLanguage.getCurrentContext();
120-
final CallStackManager callStackManager = context.getCallStack();
217+
int i = 0;
218+
for (TruffleStackTraceElement stackTraceElement : stackTrace) {
219+
if (i < omitted) {
220+
++i;
221+
continue;
222+
}
121223

122-
int i = 0;
123-
for (TruffleStackTraceElement stackTraceElement : stackTrace) {
224+
assert i != 0 || stackTraceElement.getLocation() == location;
225+
final Node callNode = stackTraceElement.getLocation();
226+
227+
if (callStackManager.ignoreFrame(callNode, stackTraceElement.getTarget())) {
124228
// TODO(norswap, 24 Dec. 2019)
125229
// Seems wrong to me, this causes frames that would otherwise be ignored to
126-
// count towards the omitted frames.
127-
if (i >= omitted) {
128-
assert i != 0 || stackTraceElement.getLocation() == location;
129-
final Node callNode = stackTraceElement.getLocation();
130-
131-
if (!callStackManager.ignoreFrame(callNode, stackTraceElement.getTarget())) {
132-
final RootNode rootNode = stackTraceElement.getTarget().getRootNode();
133-
final String methodName;
134-
if (rootNode instanceof RubyRootNode) {
135-
// Ruby backtraces do not include the class name for MRI compatibility.
136-
methodName = ((RubyRootNode) rootNode).getSharedMethodInfo().getName();
137-
} else {
138-
methodName = rootNode.getName();
139-
}
140-
141-
// TODO (eregon, 4 Feb 2019): we should not ignore foreign frames without a
142-
// call node, but print info based on the methodName and CallTarget.
143-
if (rootNode instanceof RubyRootNode || callNode != null) {
144-
activations.add(new Activation(callNode, methodName));
145-
}
146-
}
147-
148-
}
149-
i++;
230+
// count towards the omitted frames.}
231+
++i;
232+
continue;
233+
}
234+
235+
final RootNode rootNode = stackTraceElement.getTarget().getRootNode();
236+
final String methodName;
237+
if (rootNode instanceof RubyRootNode) {
238+
// Ruby backtraces do not include the class name for MRI compatibility.
239+
methodName = ((RubyRootNode) rootNode).getSharedMethodInfo().getName();
240+
} else {
241+
methodName = rootNode.getName();
150242
}
151243

152-
// If there are activations with a InternalMethod but no caller information above in the
153-
// stack, then all of these activations are internal as they are not called from user code.
154-
while (!activations.isEmpty() && activations.get(activations.size() - 1).getCallNode() == null) {
155-
activations.remove(activations.size() - 1);
244+
// TODO (eregon, 4 Feb 2019): we should not ignore foreign frames without a
245+
// call node, but print info based on the methodName and CallTarget.
246+
if (rootNode instanceof RubyRootNode || callNode != null) {
247+
activations.add(new Activation(callNode, methodName));
156248
}
157249

158-
this.activations = activations.toArray(new Activation[activations.size()]);
250+
i++;
159251
}
160252

161-
return this.activations;
253+
// If there are activations with a InternalMethod but no caller information above in the
254+
// stack, then all of these activations are internal as they are not called from user code.
255+
while (!activations.isEmpty() && activations.get(activations.size() - 1).getCallNode() == null) {
256+
activations.remove(activations.size() - 1);
257+
--i;
258+
}
259+
260+
this.totalUnderlyingActivations = i;
261+
return this.activations = activations.toArray(new Activation[activations.size()]);
262+
}
263+
264+
public Activation[] getActivations() {
265+
return getActivations(this.raiseException);
162266
}
163267

164268
/**
165269
* Returns a ruby array of {@code Thread::Backtrace::Locations} with maximum length {@code
166-
* length}, and omitting locations as requested ({@link #getOmitted()}). If more locations
167-
* are omitted than are available, return a Ruby {@code nil}.
270+
* length}, and omitting locations as requested ({@link #getOmitted()}). If more locations are
271+
* omitted than are available, return a Ruby {@code nil}.
168272
*
169273
* <p>The length can be negative, in which case it is treated as a range ending. Use -1 to
170274
* get the maximum length.
171275
*
172276
* <p>If the stack trace hasn't been filled yet, this method will fill it.
277+
*
278+
* @param length the maximum number of locations to return (if positive), or -1 minus the
279+
* number of items to exclude at the end. You can use
280+
* {@link GetBacktraceException#UNLIMITED} to signal that you want all locations.
281+
*
173282
*/
174283
public DynamicObject getBacktraceLocations(int length) {
175284

176285
final RubyContext context = RubyLanguage.getCurrentContext();
177-
178-
// NOTE(norswap, 24 Dec 2019)
179-
// Causes the stack trace to be filled if not done already.
180-
// We must call this rather than TruffleStackTrace#getStackTrace because
181-
// it does some additional filtering.
182-
// TODO(norswap, 20 Dec 2019)
183-
// Currently, only the filtering at the end is taken into account (as length reduction).
184-
// Filtering in the middle will be ignored because of how we build backtrace locations.
185286
final int activationsLength = getActivations().length;
186-
187-
// TODO(norswap, 24 Dec 2019)
188-
// This is an ugly stopgap solution - this doesn't seem solvable without refactoring #getActivations.
189-
// The issue is that omitting more locations than are available should return nil, while
190-
// omitting exactly the number of available locations should return an empty array.
191-
// This isn't even entirely correct: if we should ignore some frames from the stack trace,
192-
// then it's possible the method will return an empty array instead of nil.
193-
if (activationsLength == 0 && omitted > 0) {
194-
final int fullStackTraceLength = TruffleStackTrace.getStackTrace(
195-
new GetBacktraceException(location, GetBacktraceException.UNLIMITED)).size();
196-
if (omitted > fullStackTraceLength) {
197-
return context.getCoreLibrary().nil;
198-
}
287+
288+
// Omitting more locations than available should return nil.
289+
if (activationsLength == 0) {
290+
return omitted > totalUnderlyingActivations
291+
? context.getCoreLibrary().nil
292+
: ArrayHelpers.createEmptyArray(context);
199293
}
200294

201295
// NOTE (norswap, 18 Dec 2019)
202296
// TruffleStackTrace#getStackTrace (hence Backtrace#getActivations too) does not
203297
// always respect TruffleException#getStackTraceElementLimit(), so we need to use Math#min.
204-
// Haven't yet investigated why.
298+
// The reason: it doesn't count frames whose RootNode is internal towards the limit.
205299
final int locationsLength = length < 0
206300
? activationsLength + 1 + length
207301
: Math.min(activationsLength, length);
208-
302+
209303
final Object[] locations = new Object[locationsLength];
210304
final DynamicObjectFactory factory = context.getCoreLibrary().threadBacktraceLocationFactory;
211305
for (int i = 0; i < locationsLength; i++) {
212-
locations[i] = Layouts.THREAD_BACKTRACE_LOCATION.createThreadBacktraceLocation(
213-
factory,
214-
this,
215-
i);
306+
locations[i] = Layouts.THREAD_BACKTRACE_LOCATION.createThreadBacktraceLocation(factory, this, i);
216307
}
217308
return ArrayHelpers.createArray(context, locations, locations.length);
218309
}
219-
220-
public int getOmitted() {
221-
return omitted;
222-
}
223-
224-
public Throwable getJavaThrowable() {
225-
return javaThrowable;
226-
}
227310
}

0 commit comments

Comments
 (0)