Skip to content

Commit

Permalink
Add stacktrace (#1240)
Browse files Browse the repository at this point in the history
Start to implement StackTrace[]
  • Loading branch information
rocky authored Dec 24, 2024
1 parent 6b19e87 commit 8ca7514
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 6 deletions.
1 change: 1 addition & 0 deletions SYMBOLS_MANIFEST.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,7 @@ System`SquareSupersetEqual
System`SquareUnion
System`SquaredEuclideanDistance
System`SquaresR
System`Stacktrace
System`StandardDeviation
System`StandardForm
System`Star
Expand Down
51 changes: 46 additions & 5 deletions mathics/builtin/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from time import time
from typing import Callable

import mathics.eval.trace
import mathics.eval.tracing
from mathics.core.attributes import A_HOLD_ALL, A_HOLD_ALL_COMPLETE, A_PROTECTED
from mathics.core.builtin import Builtin
Expand Down Expand Up @@ -124,7 +125,7 @@ class PrintTrace(_TraceBase):
Note: before '$TraceBuiltins' is set to 'True', 'PrintTrace[]' will print an empty
list.
>> PrintTrace[]
>> PrintTrace[] (* See console log *)
>> $TraceBuiltins = True
= True
Expand All @@ -148,6 +149,46 @@ def eval(self, evaluation, options={}):
return SymbolNull


class Stacktrace(_TraceBase):
"""
## <url>:trace native symbol:</url>
<dl>
<dt>'Stacktrace[]'
<dd>Print Mathics3 stack trace of evalutations leading to this point
</dl>
To show the Mathics3 evaluation stack at the \
point where expression $expr$ is evaluated, wrap $expr$ inside '{$expr$ Stacktrace[]}[1]]' \
or something similar.
Here is a complete example. To show the evaluation stack when computing a homegrown \
factorial function:
>> F[0] := {1, Stacktrace[]}[[1]]; F[n_] := n * F[n-1]
>> F[3] (* See console log *)
= 6
The actual 'Stacktrace[0]' call is hidden from the output; so when \
run on its own, nothing appears.
>> Stacktrace[]
#> Clear[F]
"""

summary_text = "print Mathics3 function stacktrace"

def eval(self, evaluation: Evaluation):
"Stacktrace[]"

# Use longer-form resolve call
# so a debugger can change this.
mathics.eval.trace.eval_Stacktrace()
return SymbolNull


class TraceBuiltins(_TraceBase):
"""
## <url>:trace native symbol:</url>
Expand All @@ -168,22 +209,22 @@ class TraceBuiltins(_TraceBase):
</ul>
>> TraceBuiltins[Graphics3D[Tetrahedron[]]]
>> TraceBuiltins[Graphics3D[Tetrahedron[]]] (* See console log *)
= -Graphics3D-
By default, the output is sorted by the name:
>> TraceBuiltins[Times[x, x]]
>> TraceBuiltins[Times[x, x]] (* See console log *)
= x ^ 2
By default, the output is sorted by the number of calls of the builtin from \
highest to lowest:
>> TraceBuiltins[Times[x, x], SortBy->"count"]
>> TraceBuiltins[Times[x, x], SortBy->"count"] (* See console log *)
= x ^ 2
You can have results ordered by name, or time.
Trace an expression and list the result by time from highest to lowest.
>> TraceBuiltins[Times[x, x], SortBy->"time"]
>> TraceBuiltins[Times[x, x], SortBy->"time"] (* See console log *)
= x ^ 2
"""

Expand Down
83 changes: 83 additions & 0 deletions mathics/eval/trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
Eval routines from mathics.trace
"""

import inspect
from math import log10
from typing import Any, Tuple

from mathics.core.expression import Expression


def eval_Stacktrace():
"""
Display the Python call stack but filtered so that we Builtin calls.
"""

frame = inspect.currentframe()
assert frame is not None
frame = frame.f_back
frame_number = -2
last_was_eval = False

frames = []
while frame is not None:
is_builtin, self_obj = is_showable_frame(frame)
if is_builtin:
# The two frames are always Stacktrace[]
# and Evaluate of that. So skip these.
if frame_number > 0 and not last_was_eval:
if isinstance(self_obj, Expression):
last_was_eval = False
frame_str = self_obj
else:
last_was_eval = True
builtin_class = self_obj.__class__
mathics_builtin_name = builtin_class.__name__
eval_name = frame.f_code.co_name
if hasattr(self_obj, eval_name):
docstring = getattr(self_obj, eval_name).__doc__
docstring = docstring.replace("%(name)s", mathics_builtin_name)
args_pattern = (
docstring[len(mathics_builtin_name) + 1 : -1]
if docstring.startswith(mathics_builtin_name)
else ""
)
else:
args_pattern = ""

frame_str = f"{mathics_builtin_name}[{args_pattern}]"
frames.append(frame_str)
frame_number += 1
frame = frame.f_back

# FIXME this should done in a separate function and the
# we should return the above.
n = len(frames)
max_width = int(log10(n + 1)) + 1
number_template = "%%%dd" % max_width
for frame_number, frame_str in enumerate(frames):
formatted_frame_number = number_template % (n - frame_number)
print(f"{formatted_frame_number}: {frame_str}")
pass


def is_showable_frame(frame) -> Tuple[bool, Any]:
"""
Return True if frame is the frame for an eval() function of a
Mathics3 Builtin function, {List,}Expression.evaluate(),
or a rewrite step.
We make the check based on whether the function name starts with "eval",
has a "self" parameter and the class that self is an instance of the Builtin
class.
"""
from mathics.core.builtin import Builtin
from mathics.core.expression import Expression

if not inspect.isframe(frame):
return False, None
if not frame.f_code.co_name.startswith("eval"):
return False, None
self_obj = frame.f_locals.get("self")
return isinstance(self_obj, (Builtin, Expression)), self_obj
2 changes: 1 addition & 1 deletion mathics/packages/Combinatorica-repo

0 comments on commit 8ca7514

Please sign in to comment.