|
31 | 31 | import com.oracle.truffle.api.object.DynamicObject;
|
32 | 32 | import com.oracle.truffle.api.source.SourceSection;
|
33 | 33 |
|
| 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 | + */ |
34 | 67 | public class Backtrace {
|
35 | 68 |
|
| 69 | + // See accessors for info on most undocumented fields. |
| 70 | + |
36 | 71 | private final Node location;
|
37 |
| - /** Only set for SyntaxError, where getLocation() does not represent where the error occurred. */ |
38 | 72 | private final SourceSection sourceLocation;
|
39 |
| - private RaiseException raiseException; |
40 |
| - private Activation[] activations; |
41 | 73 | private final int omitted;
|
| 74 | + private RaiseException raiseException; |
42 | 75 | private final Throwable javaThrowable;
|
| 76 | + private Activation[] activations; |
| 77 | + |
| 78 | + /** How many activations would there be if omitted was 0? */ |
| 79 | + private int totalUnderlyingActivations; |
43 | 80 |
|
| 81 | + // region Constructors |
| 82 | + |
| 83 | + /** |
| 84 | + * Fully explicit constructor. |
| 85 | + */ |
44 | 86 | public Backtrace(Node location, SourceSection sourceLocation, int omitted, Throwable javaThrowable) {
|
45 | 87 | this.location = location;
|
46 | 88 | this.sourceLocation = sourceLocation;
|
47 | 89 | this.omitted = omitted;
|
48 | 90 | this.javaThrowable = javaThrowable;
|
49 | 91 | }
|
50 | 92 |
|
| 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 | + */ |
51 | 101 | public Backtrace(TruffleException exception) {
|
52 | 102 | this.location = exception.getLocation();
|
53 | 103 | this.sourceLocation = exception.getSourceLocation();
|
54 | 104 | this.omitted = 0;
|
55 | 105 | this.javaThrowable = null;
|
56 |
| - |
57 | 106 | this.activations = getActivations((Throwable) exception);
|
58 | 107 | }
|
59 | 108 |
|
| 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 | + */ |
60 | 114 | public Backtrace(Throwable exception) {
|
61 | 115 | this.location = null;
|
62 | 116 | this.sourceLocation = null;
|
63 | 117 | this.omitted = 0;
|
64 | 118 | this.javaThrowable = null;
|
65 |
| - |
66 | 119 | this.activations = getActivations(exception);
|
67 | 120 | }
|
68 | 121 |
|
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 |
85 | 124 |
|
| 125 | + /** |
| 126 | + * AST node that caused the associated exception, if the info is available, or null. |
| 127 | + */ |
86 | 128 | public Node getLocation() {
|
87 | 129 | return location;
|
88 | 130 | }
|
89 | 131 |
|
| 132 | + /** |
| 133 | + * Only set for {@code SyntaxError}, where it represents where the error occurred |
| 134 | + * (while {@link #getLocation()} does not). |
| 135 | + */ |
90 | 136 | public SourceSection getSourceLocation() {
|
91 | 137 | return sourceLocation;
|
92 | 138 | }
|
93 | 139 |
|
| 140 | + /** |
| 141 | + * Returns the wrapper for the Ruby exception associated with this backtrace, if any, and |
| 142 | + * null otherwise. |
| 143 | + */ |
94 | 144 | public RaiseException getRaiseException() {
|
95 | 145 | return raiseException;
|
96 | 146 | }
|
97 | 147 |
|
| 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 | + */ |
98 | 153 | public void setRaiseException(RaiseException raiseException) {
|
99 | 154 | assert this.raiseException == null : "the RaiseException of a Backtrace must not be set again, otherwise the original backtrace is lost";
|
100 | 155 | this.raiseException = raiseException;
|
101 | 156 | }
|
102 | 157 |
|
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; |
105 | 196 | }
|
106 | 197 |
|
107 | 198 | @TruffleBoundary
|
108 | 199 | 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; |
113 | 212 |
|
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(); |
117 | 216 |
|
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 | + } |
121 | 223 |
|
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())) { |
124 | 228 | // TODO(norswap, 24 Dec. 2019)
|
125 | 229 | // 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(); |
150 | 242 | }
|
151 | 243 |
|
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)); |
156 | 248 | }
|
157 | 249 |
|
158 |
| - this.activations = activations.toArray(new Activation[activations.size()]); |
| 250 | + i++; |
159 | 251 | }
|
160 | 252 |
|
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); |
162 | 266 | }
|
163 | 267 |
|
164 | 268 | /**
|
165 | 269 | * 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}. |
168 | 272 | *
|
169 | 273 | * <p>The length can be negative, in which case it is treated as a range ending. Use -1 to
|
170 | 274 | * get the maximum length.
|
171 | 275 | *
|
172 | 276 | * <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 | + * |
173 | 282 | */
|
174 | 283 | public DynamicObject getBacktraceLocations(int length) {
|
175 | 284 |
|
176 | 285 | 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. |
185 | 286 | 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); |
199 | 293 | }
|
200 | 294 |
|
201 | 295 | // NOTE (norswap, 18 Dec 2019)
|
202 | 296 | // TruffleStackTrace#getStackTrace (hence Backtrace#getActivations too) does not
|
203 | 297 | // 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. |
205 | 299 | final int locationsLength = length < 0
|
206 | 300 | ? activationsLength + 1 + length
|
207 | 301 | : Math.min(activationsLength, length);
|
208 |
| - |
| 302 | + |
209 | 303 | final Object[] locations = new Object[locationsLength];
|
210 | 304 | final DynamicObjectFactory factory = context.getCoreLibrary().threadBacktraceLocationFactory;
|
211 | 305 | 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); |
216 | 307 | }
|
217 | 308 | return ArrayHelpers.createArray(context, locations, locations.length);
|
218 | 309 | }
|
219 |
| - |
220 |
| - public int getOmitted() { |
221 |
| - return omitted; |
222 |
| - } |
223 |
| - |
224 |
| - public Throwable getJavaThrowable() { |
225 |
| - return javaThrowable; |
226 |
| - } |
227 | 310 | }
|
0 commit comments