From 10d9e04002df37124974c771e608707cb6397b4c Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sat, 31 Aug 2024 22:02:33 -0400 Subject: [PATCH] Add event handler (#1074) Code in support of event tracing debugging. Used b y Mathics3 Module pymtathics.debugger in repository Mathics3/mathics-debugger. --- mathics/builtin/numbers/calculus.py | 20 ++--- mathics/builtin/specialfns/gamma.py | 6 +- mathics/core/builtin.py | 24 +++--- mathics/core/symbols.py | 8 +- mathics/doc/__init__.py | 2 +- mathics/eval/arithmetic.py | 14 ++-- mathics/eval/numbers/numbers.py | 25 +++++++ mathics/eval/tracing.py | 110 ++++++++++++++++++++++++++++ mathics/timing.py | 8 +- 9 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 mathics/eval/tracing.py diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 55e3dd3f6..7fb5c340c 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -15,6 +15,7 @@ import numpy as np import sympy +import mathics.eval.tracing as tracing from mathics.builtin.scoping import dynamic_scoping from mathics.core.atoms import ( Atom, @@ -529,7 +530,7 @@ def to_sympy(self, expr, **kwargs): sym_d_args.append(count) try: - return sympy.Derivative(sym_func, *sym_d_args) + return tracing.run_sympy(sympy.Derivative, sym_func, *sym_d_args) except ValueError: return @@ -999,6 +1000,7 @@ class Integrate(SympyFunction): >> N[Integrate[Sin[Exp[-x^2 /2 ]],{x,1,2}]] = 0.330804 """ + # Reinstate as a unit test or describe why it should be an example and fix. # >> Integrate[x/Exp[x^2/t], {x, 0, Infinity}] # = ConditionalExpression[t / 2, Abs[Arg[t]] < Pi / 2] @@ -1088,7 +1090,7 @@ def eval(self, f, xs, evaluation: Evaluation, options: dict): else: vars.append((x, a, b)) try: - sympy_result = sympy.integrate(f_sympy, *vars) + sympy_result = tracing.run_sympy(sympy.integrate, f_sympy, *vars) pass except sympy.PolynomialError: return @@ -1097,7 +1099,7 @@ def eval(self, f, xs, evaluation: Evaluation, options: dict): return except NotImplementedError: # e.g. NotImplementedError: Result depends on the sign of - # -sign(_Mathics_User_j)*sign(_Mathics_User_w) + # -sign(_u`j)*sign(_u`w) return if prec is not None and isinstance(sympy_result, sympy.Integral): # TODO MaxExtraPrecision -> maxn @@ -1245,7 +1247,7 @@ def eval(self, expr, x, x0, evaluation: Evaluation, options={}): return try: - result = sympy.limit(expr, x, x0, dir_sympy) + result = tracing.run_sympy(sympy.limit, expr, x, x0, dir_sympy) except sympy.PoleError: pass except RuntimeError: @@ -1630,7 +1632,7 @@ def eval(self, f, i, evaluation: Evaluation): evaluation.message("Root", "iidx", i) return - r = sympy.CRootOf(poly.to_sympy(), idx) + r = tracing.run_sympy(sympy.CRootOf, poly.to_sympy(), idx) except sympy.PolynomialError: evaluation.message("Root", "nuni", f) return @@ -1661,7 +1663,7 @@ def to_sympy(self, expr, **kwargs): if i is None: return None - return sympy.CRootOf(poly, i) + return tracing.run_sympy(sympy.CRootOf, poly, i) except Exception: return None @@ -2243,8 +2245,8 @@ def eval(self, eqs, vars, evaluation: Evaluation): if left is None or right is None: return eq = left - right - eq = sympy.together(eq) - eq = sympy.cancel(eq) + eq = tracing.run_sympy(sympy.together, eq) + eq = tracing.run_sympy(sympy.cancel, eq) sympy_eqs.append(eq) _, denom = eq.as_numer_denom() sympy_denoms.append(denom) @@ -2302,7 +2304,7 @@ def transform_solution(sol): if isinstance(sympy_eqs, bool): result = sympy_eqs else: - result = sympy.solve(sympy_eqs, vars_sympy) + result = tracing.run_sympy(sympy.solve, sympy_eqs, vars_sympy) if not isinstance(result, list): result = [result] if isinstance(result, list) and len(result) == 1 and result[0] is True: diff --git a/mathics/builtin/specialfns/gamma.py b/mathics/builtin/specialfns/gamma.py index 388026b0f..9c51c13e5 100644 --- a/mathics/builtin/specialfns/gamma.py +++ b/mathics/builtin/specialfns/gamma.py @@ -23,7 +23,7 @@ from mathics.core.number import FP_MANTISA_BINARY_DIGITS, dps, min_prec from mathics.core.symbols import Symbol, SymbolSequence from mathics.core.systemsymbols import SymbolAutomatic, SymbolGamma -from mathics.eval.arithmetic import call_mpmath +from mathics.eval.arithmetic import run_mpmath from mathics.eval.nevaluator import eval_N from mathics.eval.numerify import numerify @@ -110,7 +110,7 @@ def eval_with_z(self, z, a, b, evaluation): if None in float_args: return - result = call_mpmath( + result = run_mpmath( mpmath_function, tuple(float_args), FP_MANTISA_BINARY_DIGITS ) else: @@ -121,7 +121,7 @@ def eval_with_z(self, z, a, b, evaluation): mpmath_args = [x.to_mpmath() for x in args] if None in mpmath_args: return - result = call_mpmath(mpmath_function, tuple(mpmath_args), prec) + result = run_mpmath(mpmath_function, tuple(mpmath_args), prec) return result diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index 78504c0e0..f287a5fac 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -10,7 +10,7 @@ import os.path as osp import re from abc import ABC -from functools import lru_cache, total_ordering +from functools import total_ordering from itertools import chain from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, cast @@ -18,6 +18,14 @@ import pkg_resources import sympy +# Note: it is important *not* to use: +# from mathics.eval.tracing import run_sympy +# but, instead, import the module, as below, and then +# access ``run_sympy`` using ``tracing.run_sympy.`` +# +# This allows us to change where ``tracing.run_sympy`` points to at +# run time. +import mathics.eval.tracing as tracing from mathics.core.atoms import ( Integer, Integer0, @@ -530,7 +538,7 @@ def eval(self, z, evaluation: Evaluation): sympy_args = to_numeric_sympy_args(z, evaluation) sympy_fn = getattr(sympy, self.sympy_name) try: - return from_sympy(run_sympy(sympy_fn, *sympy_args)) + return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args)) except Exception: return @@ -564,7 +572,7 @@ def to_sympy(self, expr, **kwargs): return None sympy_function = self.get_sympy_function(elements) if sympy_function is not None: - return sympy_function(*sympy_args) + return tracing.run_sympy(sympy_function, *sympy_args) except TypeError: pass @@ -590,7 +598,6 @@ class MPMathFunction(SympyFunction): mpmath_name = None nargs = {1} - @lru_cache(maxsize=1024) def get_mpmath_function(self, args): if self.mpmath_name is None or len(args) not in self.nargs: return None @@ -1111,15 +1118,6 @@ def test(self, expr) -> bool: raise NotImplementedError -@lru_cache() -def run_sympy(sympy_fn: Callable, *sympy_args) -> Any: - """ - Wrapper to run a SymPy function with a cache. - TODO: hook into SymPyTracing -> True - """ - return sympy_fn(*sympy_args) - - class PatternError(Exception): def __init__(self, name, tag, *args): super().__init__() diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index ed00a4988..193231d3b 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -14,8 +14,12 @@ # I put this constants here instead of inside `mathics.core.convert.sympy` # to avoid a circular reference. Maybe they should be in its own module. -sympy_symbol_prefix = "_mu_" -sympy_slot_prefix = "_ms_" +# Prefix used for Sympy variables. We want prefixes to be short to +# keep variable names short. In tracing values, long names makes +# output messy and harder to follow, since it detracts from the +# important information +sympy_symbol_prefix = "_u" +sympy_slot_prefix = "_#" # FIXME: This is repeated below diff --git a/mathics/doc/__init__.py b/mathics/doc/__init__.py index 1be93c620..50ff02cfd 100644 --- a/mathics/doc/__init__.py +++ b/mathics/doc/__init__.py @@ -22,7 +22,7 @@ documentation tests is done elsewhere. FIXME: This code should be replaced by Sphinx and autodoc. -Things are such a mess, that it is too difficult to contemplate this right now. +Things are such a mess, that it is too difficult to contemplate this right now. Also there higher-priority flaws that are more more pressing. In the shorter, we might we move code for extracting printing to a separate package. diff --git a/mathics/eval/arithmetic.py b/mathics/eval/arithmetic.py index 3a219c03d..a80f5bd2e 100644 --- a/mathics/eval/arithmetic.py +++ b/mathics/eval/arithmetic.py @@ -7,12 +7,15 @@ used just as a last resource. """ -from functools import lru_cache from typing import Callable, List, Optional, Tuple import mpmath import sympy +# Note: it is important *not* use: from mathics.eval.tracing import run_sympy +# but instead import the module and access below as tracing.run_sympy. +# This allows us change where tracing.run_sympy points at runtime. +import mathics.eval.tracing as tracing from mathics.core.atoms import ( NUMERICAL_CONSTANTS, Complex, @@ -50,8 +53,7 @@ # This cache might not be used that much. -@lru_cache() -def call_mpmath( +def run_mpmath( mpmath_function: Callable, mpmath_args: tuple, precision: int ) -> Optional[BaseElement]: """ @@ -63,7 +65,7 @@ def call_mpmath( """ with mpmath.workprec(precision): try: - result_mp = mpmath_function(*mpmath_args) + result_mp = tracing.run_mpmath(mpmath_function, *mpmath_args) if precision != FP_MANTISA_BINARY_DIGITS: return from_mpmath(result_mp, precision) return from_mpmath(result_mp) @@ -337,12 +339,12 @@ def eval_mpmath_function( if None in float_args: return - return call_mpmath(mpmath_function, tuple(float_args), FP_MANTISA_BINARY_DIGITS) + return run_mpmath(mpmath_function, tuple(float_args), FP_MANTISA_BINARY_DIGITS) else: mpmath_args = [x.to_mpmath(prec) for x in args] if None in mpmath_args: return - return call_mpmath(mpmath_function, tuple(mpmath_args), prec) + return run_mpmath(mpmath_function, tuple(mpmath_args), prec) def eval_Plus(*items: BaseElement) -> BaseElement: diff --git a/mathics/eval/numbers/numbers.py b/mathics/eval/numbers/numbers.py index 628043d4e..fe79ec447 100644 --- a/mathics/eval/numbers/numbers.py +++ b/mathics/eval/numbers/numbers.py @@ -8,6 +8,10 @@ import mpmath import sympy +# Note: it is important *not* use: from mathics.eval.tracing import run_sympy +# but instead import the module and access below as tracing.run_sympy. +# This allows us change where tracing.run_sympy points at runtime. +import mathics.eval.tracing as tracing from mathics.core.atoms import Complex, MachineReal, PrecisionReal from mathics.core.convert.sympy import from_sympy from mathics.core.element import BaseElement @@ -136,6 +140,27 @@ def cancel(expr): return expr +def cancel(expr): + if expr.has_form("Plus", None): + return Expression(SymbolPlus, *[cancel(element) for element in expr.elements]) + else: + try: + result = expr.to_sympy() + if result is None: + return None + + # result = sympy.powsimp(result, deep=True) + result = tracing.run_sympy(sympy.cancel, result) + + # cancel factors out rationals, so we factor them again + result = sympy_factor(result) + + return from_sympy(result) + except sympy.PolynomialError: + # e.g. for non-commutative expressions + return expr + + def sympy_factor(expr_sympy): try: result = sympy.together(expr_sympy) diff --git a/mathics/eval/tracing.py b/mathics/eval/tracing.py new file mode 100644 index 000000000..7dc2d6907 --- /dev/null +++ b/mathics/eval/tracing.py @@ -0,0 +1,110 @@ +""" +Debug Tracing and Trace-Event handlers. + +This is how we support external (Mathics3 module) debuggers and tracers. +""" + +import inspect +from enum import Enum +from typing import Any, Callable, Optional + +TraceEventNames = ("SymPy", "Numpy", "mpmath", "apply", "debugger") +TraceEvent = Enum("TraceEvent", TraceEventNames) + + +hook_entry_fn: Optional[Callable] = None +hook_exit_fn: Optional[Callable] = None + + +def trace_fn_call_event(func: Callable) -> Callable: + """ + Wrap a call event with callbacks, + so we can track what happened before the call and + the result returned by the call. + + A traced function could be a sympy or mpmath call or + maybe a bulltin-function call. + """ + + def wrapper(*args) -> Any: + skip_call = False + result = None + event_type = args[0] + if hook_entry_fn is not None: + skip_call = hook_entry_fn(*args) + if not skip_call: + result = func(*args[1:]) + if hook_exit_fn is not None: + result = hook_exit_fn(event_type, result) + return result + + return wrapper + + +@trace_fn_call_event +def trace_call(fn: Callable, *args) -> Any: + """ + Runs a function inside a decorator that + traps call and return information that can be used in + a tracer or debugger + """ + return fn(*args) + + +def call_event_print(event: TraceEvent, fn: Callable, *args) -> bool: + """ + A somewhat generic function to show an event-traced call. + """ + if type(fn) == type or inspect.ismethod(fn) or inspect.isfunction(fn): + name = f"{fn.__module__}.{fn.__qualname__}" + else: + name = str(fn) + print(f"{event.name} call : {name}{args[:3]}") + return False + + +def return_event_print(event: TraceEvent, result: Any) -> Any: + """ + A somewhat generic function to print a traced call's + return value. + """ + print(f"{event.name} result: {result}") + return result + + +def run_fast(fn: Callable, *args) -> Any: + """ + Fast-path call to run a event-tracable function, but no tracing is + in effect. This add another level of indirection to + some function calls, but Jit'ing will probably remove this + when it is a bottleneck. + """ + return fn(*args) + + +def run_mpmath_traced(fn: Callable, *args) -> Any: + return trace_call(TraceEvent.mpmath, fn, *args) + + +def run_sympy_traced(fn: Callable, *args) -> Any: + return trace_call(TraceEvent.SymPy, fn, *args) + + +# The below functions are changed by a tracer or debugger +# to get information from traced functions. +# These have to be defined. +run_sympy: Callable = run_fast +run_mpmath: Callable = run_fast + +# If you want to test without using Mathics3 debugger module: + +# import os +# if os.environ.get("MATHICS3_SYMPY_TRACE", None) is not None: +# hook_entry_fn = call_event_print +# hook_exit_fn = return_event_print +# run_sympy: Callable = run_sympy_traced + +# if os.environ.get("MATHICS3_MPMATH_TRACE", None) is not None: +# hook_entry_fn = call_event_print +# hook_exit_fn = return_event_print +# run_mpmath: Callable = run_mpmath_traced diff --git a/mathics/timing.py b/mathics/timing.py index f33bc8528..9ab7eb089 100644 --- a/mathics/timing.py +++ b/mathics/timing.py @@ -67,15 +67,13 @@ def show_lru_cache_statistics(): """ from mathics.builtin.atomic.numbers import log_n_b from mathics.core.atoms import Integer, Rational - from mathics.core.builtin import MPMathFunction, run_sympy + from mathics.core.builtin import MPMathFunction from mathics.core.convert.mpmath import from_mpmath - from mathics.eval.arithmetic import call_mpmath + from mathics.eval.arithmetic import run_mpmath print(f"Integer {len(Integer._integers)}") print(f"Rational {len(Rational._rationals)}") - print(f"call_mpmath {call_mpmath.cache_info()}") + print(f"run_mpmath {run_mpmath.cache_info()}") print(f"log_n_b {log_n_b.cache_info()}") print(f"from_mpmath {from_mpmath.cache_info()}") print(f"get_mpmath_function {MPMathFunction.get_mpmath_function.cache_info()}") - - print(f"run_sympy {run_sympy.cache_info()}")