From 6c024344e5029e7a7d15b11f226fe53d617f5f6b Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Mon, 16 Sep 2024 20:40:37 -0400 Subject: [PATCH] BuiltinRule -> FunctionApplyRule (#1085) The name Builtin is vague, since there are things that are "builtin" (like variables and symbols) that are not functions. Furthermore, BuiltinRule handles functions added by Mathics3 modules. --- mathics/builtin/trace.py | 8 ++-- mathics/core/builtin.py | 10 ++--- mathics/core/rules.py | 92 ++++++++++++++++++++++++++++++---------- mathics/main.py | 4 +- 4 files changed, 80 insertions(+), 34 deletions(-) diff --git a/mathics/builtin/trace.py b/mathics/builtin/trace.py index eb5968045..517e7ba03 100644 --- a/mathics/builtin/trace.py +++ b/mathics/builtin/trace.py @@ -29,7 +29,7 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.rules import BuiltinRule +from mathics.core.rules import FunctionApplyRule from mathics.core.symbols import SymbolFalse, SymbolNull, SymbolTrue, strip_context @@ -240,11 +240,11 @@ def sort_by_name(tup: tuple): @staticmethod def enable_trace(evaluation) -> None: if TraceBuiltins.traced_definitions is None: - TraceBuiltins.apply_function_copy = BuiltinRule.apply_function + TraceBuiltins.apply_function_copy = FunctionApplyRule.apply_function TraceBuiltins.definitions_copy = evaluation.definitions # Replaces apply_function by the custom one - BuiltinRule.apply_function = traced_apply_function + FunctionApplyRule.apply_function = traced_apply_function # Create new definitions uses the new apply_function evaluation.definitions = Definitions(add_builtin=True) else: @@ -252,7 +252,7 @@ def enable_trace(evaluation) -> None: @staticmethod def disable_trace(evaluation) -> None: - BuiltinRule.apply_function = TraceBuiltins.apply_function_copy + FunctionApplyRule.apply_function = TraceBuiltins.apply_function_copy evaluation.definitions = TraceBuiltins.definitions_copy def eval(self, expr, evaluation, options={}): diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index f287a5fac..2a7c0cf46 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -55,7 +55,7 @@ from mathics.core.number import PrecisionValueError, dps, get_precision, min_prec from mathics.core.parser.util import PyMathicsDefinitions, SystemDefinitions from mathics.core.pattern import Pattern -from mathics.core.rules import BuiltinRule, Rule +from mathics.core.rules import FunctionApplyRule, Rule from mathics.core.symbols import ( BaseElement, BooleanType, @@ -121,7 +121,7 @@ def eval(x, evaluation): return Expression(Symbol("G"), x*2) ``` - adds a ``BuiltinRule`` to the symbol's definition object that implements + adds a ``FunctionApplyRule`` to the symbol's definition object that implements ``F[x_]->G[x*2]``. As shown in the example above, leading argument names of the @@ -269,11 +269,11 @@ def contribute(self, definitions, is_pymodule=False): prefix="eval", is_pymodule=is_pymodule ): rules.append( - BuiltinRule(name, pattern, function, check_options, system=True) + FunctionApplyRule(name, pattern, function, check_options, system=True) ) for pattern, function in self.get_functions(is_pymodule=is_pymodule): rules.append( - BuiltinRule(name, pattern, function, check_options, system=True) + FunctionApplyRule(name, pattern, function, check_options, system=True) ) for pattern_str, replace_str in self.rules.items(): pattern_str = pattern_str % {"name": name} @@ -325,7 +325,7 @@ def contextify_form_name(f): if form not in formatvalues: formatvalues[form] = [] formatvalues[form].append( - BuiltinRule(name, pattern, function, None, system=True) + FunctionApplyRule(name, pattern, function, None, system=True) ) for pattern, replace in self.formats.items(): forms, pattern = extract_forms(pattern) diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 6823f29b1..6af8f5ff5 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -1,11 +1,51 @@ # -*- coding: utf-8 -*- -"""Rules are a core part of the way WMA and Mathics3 executes a -program. Expressions can be transformed by rewrite rules (AKA -transformation rules); builtin functions get matched and applied via a -function signature specified using a BuiltinRule. +"""Rules are a core part of the way Mathematica and Mathics3 execute a +program. + +Expressions which are transformed by rewrite rules (AKA transformation +rules) are handed by the `Rule` class. + +There are also rules for how to match, assign function parameter +arguments, and then apply a Python "evaluation" function to a Mathics3 Expression. +These kinds of rules are handled by objects in the `FunctionApplyRule` class. This module contains the classes for these two types of rules. +In a `FunctionApplyRule` rule, the match status of a rule depends on the evaluation return. + +For example, suppose that we try to apply rule `F[x_]->x^2` to the expression `F[2]`. The pattern part of the rule,`F[x_]` matches +the expression, `Blank[x]` (or `x_`) is replaced by `2`, giving the substitution expression `2^2`. Evaluation then stops +looking for other rules to be applied over `F[2]`. + +On the other hand, suppose that we define a `FunctionApplyRule` that associates `F[x_]` with the function: + +``` +... +class MyFunction(Builtin): + ... + def eval_f(self, x, evaluation) -> Optional[Expression]: + "F[x_]" # pattern part of FunctionApplyRule + if x>3: + return Expression(SymbolPower, x, Integer2) + return None +``` + +Then, if we apply the rule to `F[2]`, the function is evaluated returning `None`. Then, in the evaluation loop, we get the same +effect as if the pattern didn't match with the expression. The loop continues then with the next rule associated with `F`. + +Why do things this way? + +Sometimes, the cost of deciding if the rule match is similar to the cost of evaluating the function. Suppose for example a rule + + F[x_/;(G[x]>0)]:=G[x] + +with G[x] a computationally expensive function. To decide if G[x] is larger than 0, we need to evaluate it, +and once we have evaluated it, just need to return its value. + +Also, this allows us to handle several rules in the same function, without relying on our very slow pattern-matching routines. +In particular, this is used for for some critical low-level tasks like building lists in iterators, processing arithmetic expressions, +plotting functions, or evaluating derivatives and integrals. + """ @@ -38,16 +78,15 @@ class StopGenerator_BaseRule(StopGenerator): class BaseRule(KeyComparable, ABC): - """ - This is the base class from which BuiltinRule and Rule classes - are derived from. + """This is the base class from which the FunctionApplyRule and + Rule classes are derived from. Rules are part of the rewriting system of Mathics3. See https://en.wikipedia.org/wiki/Rewriting - This class is not complete in of itself and subclasses should - adapt or fill in what is needed. In particular either ``apply_rule()`` - or ``apply_function()`` need to be implemented. + This class is not complete in of itself; subclasses must adapt or + fill in what is needed. In particular either ``apply_rule()`` or + ``apply_function()`` need to be implemented. Note: we want Rules to be serializable so that we can dump and restore Rules in order to make startup time faster. @@ -89,7 +128,7 @@ def yield_match(vars, rest): del vars[name] apply_fn = ( self.apply_function - if isinstance(self, BuiltinRule) + if isinstance(self, FunctionApplyRule) else self.apply_rule ) new_expression = apply_fn(expression, vars, options, evaluation) @@ -137,10 +176,14 @@ def yield_match(vars, rest): else: return None - def apply_rule(self): + def apply_rule( + self, expression: BaseElement, vars: dict, options: dict, evaluation: Evaluation + ): raise NotImplementedError - def apply_function(self): + def apply_function( + self, expression: BaseElement, vars: dict, options: dict, evaluation: Evaluation + ): raise NotImplementedError def get_sort_key(self) -> tuple: @@ -216,11 +259,11 @@ def __repr__(self) -> str: return " %s>" % (self.pattern, self.replace) -# FIXME: the class name would be better called FunctionCallRule. -class BuiltinRule(BaseRule): +class FunctionApplyRule(BaseRule): """ - A BuiltinRule is a rule that has a replacement term that is associated - a Python function rather than a Mathics Expression as happens in a Rule. + A FunctionApplyRule is a rule that has a replacement term that + is associated a Python function rather than a Mathics Expression + as happens in a transformation Rule. Each time the Pattern part of the Rule matches an Expression, the matching subexpression is replaced by the expression returned @@ -229,7 +272,7 @@ class BuiltinRule(BaseRule): Parameters for the function are bound to parameters matched by the pattern. Here is an example taken from the symbol ``System`Plus``. - It has has associated a BuiltinRule:: + It has has associated a FunctionApplyRule:: Plus[items___] -> mathics.builtin.arithfns.basic.Plus.apply @@ -242,8 +285,8 @@ class BuiltinRule(BaseRule): The return value of this function is ``Times[2, a]`` (or more compactly: ``2*a``). When replaced in the original expression, the result is: ``F[2*a]``. - In contrast to Rule, BuiltinRules can change the state of definitions - in the the system. + In contrast to (transformation) Rules, FunctionApplyRules can + change the state of definitions in the the system. For example, the rule:: @@ -254,8 +297,9 @@ class BuiltinRule(BaseRule): sets the attribute ``NumericFunction`` in the definition of the symbol ``F`` and returns Null (``SymbolNull`)`. - This will cause `Expression.evalate() to perform an additional + This will cause `Expression.evaluate() to perform an additional ``rewrite_apply_eval()`` step. + """ def __init__( @@ -267,7 +311,9 @@ def __init__( system: bool = False, evaluation: Optional[Evaluation] = None, ) -> None: - super(BuiltinRule, self).__init__(pattern, system=system, evaluation=evaluation) + super(FunctionApplyRule, self).__init__( + pattern, system=system, evaluation=evaluation + ) self.name = name self.function = function self.check_options = check_options @@ -293,7 +339,7 @@ def apply_function( return self.function(evaluation=evaluation, **vars_noctx) def __repr__(self) -> str: - return " %s>" % (self.pattern, self.function) + return " %s>" % (self.pattern, self.function) def __getstate__(self): odict = self.__dict__.copy() diff --git a/mathics/main.py b/mathics/main.py index 3e6270b4c..3dcfc8069 100755 --- a/mathics/main.py +++ b/mathics/main.py @@ -27,7 +27,7 @@ from mathics.core.load_builtin import import_and_load_builtins from mathics.core.parser import MathicsFileLineFeeder, MathicsLineFeeder from mathics.core.read import channel_to_stream -from mathics.core.rules import BuiltinRule +from mathics.core.rules import FunctionApplyRule from mathics.core.streams import stream_manager from mathics.core.symbols import SymbolNull, strip_context from mathics.eval.files_io.files import set_input_var @@ -385,7 +385,7 @@ def main() -> int: extension_modules = default_pymathics_modules if args.trace_builtins: - BuiltinRule.apply_rule = traced_apply_function + FunctionApplyRule.apply_rule = traced_apply_function def dump_tracing_stats(): TraceBuiltins.dump_tracing_stats(sort_by="count", evaluation=None)