7
7
from typing import TYPE_CHECKING as _TYPE_CHECKING
8
8
from typing import TypeVar
9
9
10
+ from protosym .core .exceptions import NoEvaluationRuleError
10
11
from protosym .core .tree import forward_graph
11
12
from protosym .core .tree import TreeAtom
13
+ from protosym .core .tree import TreeExpr
12
14
13
15
14
16
if _TYPE_CHECKING :
15
- from protosym .core .tree import TreeExpr
16
17
from protosym .core .atom import AnyValue as _AnyValue
17
18
from protosym .core .atom import AtomType
18
19
@@ -61,7 +62,17 @@ def add_opn(self, head: TreeExpr, func: OpN[_T]) -> None:
61
62
"""Add an evaluation rule for a particular head."""
62
63
self .operations [head ] = (func , False )
63
64
64
- def call (self , head : TreeExpr , argvals : Iterable [_T ]) -> _T :
65
+ def eval_atom (self , atom : TreeAtom [_S ]) -> _T :
66
+ """Evaluate an atom."""
67
+ atom_value = atom .value
68
+ atom_func = self .atoms .get (atom_value .atom_type ) # type: ignore
69
+ if atom_func is not None :
70
+ return atom_func (atom_value .value )
71
+ else :
72
+ msg = "No rule for AtomType: " + atom_value .atom_type .name
73
+ raise NoEvaluationRuleError (msg )
74
+
75
+ def eval_operation (self , head : TreeExpr , argvals : Iterable [_T ]) -> _T :
65
76
"""Evaluate one function with some values."""
66
77
op_func , star_args = self .operations [head ]
67
78
if star_args :
@@ -81,15 +92,13 @@ def eval_recursive(self, expr: TreeExpr, values: dict[TreeExpr, _T]) -> _T:
81
92
return values [expr ]
82
93
elif isinstance (expr , TreeAtom ):
83
94
# Convert an Atom to _T
84
- value = expr .value
85
- atom_func = self .atoms [value .atom_type ]
86
- return atom_func (value .value )
95
+ return self .eval_atom (expr )
87
96
else :
88
97
# Recursively evaluate children and then apply this operation.
89
98
head = expr .children [0 ]
90
99
children = expr .children [1 :]
91
100
argvals = [self .eval_recursive (c , values ) for c in children ]
92
- return self .call (head , argvals )
101
+ return self .eval_operation (head , argvals )
93
102
94
103
def eval_forward (self , expr : TreeExpr , values : dict [TreeExpr , _T ]) -> _T :
95
104
"""Evaluate the expression using forward evaluation."""
@@ -103,15 +112,13 @@ def eval_forward(self, expr: TreeExpr, values: dict[TreeExpr, _T]) -> _T:
103
112
if value_get is not None :
104
113
value = value_get
105
114
else :
106
- atom_value = atom .value # type:ignore
107
- atom_func = self .atoms [atom_value .atom_type ]
108
- value = atom_func (atom_value .value )
115
+ value = self .eval_atom (atom ) # type: ignore
109
116
stack .append (value )
110
117
111
118
# Run forward evaluation through the operations
112
119
for head , indices in graph .operations :
113
120
argvals = [stack [i ] for i in indices ]
114
- stack .append (self .call (head , argvals ))
121
+ stack .append (self .eval_operation (head , argvals ))
115
122
116
123
# Now stack is the values of the topological sort of expr and stack[-1]
117
124
# is the value of expr.
@@ -124,3 +131,57 @@ def __call__(
124
131
if values is None :
125
132
values = {}
126
133
return self .evaluate (expr , values )
134
+
135
+
136
+ class Transformer (Evaluator [TreeExpr ]):
137
+ """Specialized Evaluator for TreeExpr -> TreeExpr operations.
138
+
139
+ Whereas :class:`Evaluator` is used to evaluate an expression into a
140
+ different type of object like ``float`` or ``str`` a :class:`Transformer`
141
+ is used to transform a :class:`TreeExpr` into a new :class:`TreeExpr`.
142
+
143
+ The difference between using ``Transformer`` and using
144
+ ``Evaluator[TreeExpr]`` is that ``Transformer`` allows processing
145
+ operations that have no associated rules leaving the expression unmodified.
146
+
147
+ Examples
148
+ ========
149
+
150
+ We first import the pieces and define some functions and symbols.
151
+
152
+ >>> from protosym.core.tree import funcs_symbols
153
+ >>> from protosym.core.evaluate import Evaluator, Transformer
154
+ >>> [f, g], [x, y] = funcs_symbols(['f', 'g'], ['x', 'y'])
155
+
156
+ Now make a :class:`Transformer` to replace ``f(...)`` with ``g(...)``.
157
+
158
+ >>> f2g = Transformer()
159
+ >>> f2g.add_opn(f, lambda args: g(*args))
160
+ >>> expr = f(g(x, f(y)), y)
161
+ >>> print(expr)
162
+ f(g(x, f(y)), y)
163
+ >>> print(f2g(expr))
164
+ g(g(x, g(y)), y)
165
+
166
+ By contrast with ``Evaluator[TreeExpr]`` the above would fail because no
167
+ rule has been defined for the head ``g`` or for ``Symbol`` (the
168
+ :class:`AtomType` of ``x`` and ``y``).
169
+
170
+ >>> f2g_eval = Evaluator[TreeExpr]()
171
+ >>> f2g_eval.add_opn(f, lambda args: g(*args))
172
+ >>> f2g_eval(expr)
173
+ Traceback (most recent call last):
174
+ ...
175
+ protosym.core.exceptions.NoEvaluationRuleError: No rule for AtomType: Symbol
176
+ """
177
+
178
+ def eval_atom (self , atom : TreeAtom [_S ]) -> TreeExpr :
179
+ """Return the atom as is."""
180
+ return atom
181
+
182
+ def eval_operation (self , head : TreeExpr , argvals : Iterable [TreeExpr ]) -> TreeExpr :
183
+ """Return unevaluated operation if no rule supplied."""
184
+ if head not in self .operations :
185
+ return head (* argvals )
186
+ else :
187
+ return super ().eval_operation (head , argvals )
0 commit comments