PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
expressions.py
1"""
2Expression manipulation functions.
3
4These functions are independent of PYFT and PYFTscope objects and provide
5low-level utilities for creating and manipulating FORTRAN expression XML nodes.
6"""
7
8import re
9from functools import lru_cache
10import copy
11import xml.etree.ElementTree as ET
12
13from pyfortool.util import debugDecor, isint, isfloat, fortran2xml, PYFTError
14from pyfortool import NAMESPACE
15
16
17def createElem(tagName, text=None, tail=None, childs=None):
18 """
19 Create an XML element with the given tag and attributes.
20
21 Parameters
22 ----------
23 tagName : str
24 XML tag name (without namespace).
25 text : str, optional
26 Text content for the element.
27 tail : str, optional
28 Tail text (text after element).
29 childs : Element or list, optional
30 Child element(s) to append.
31
32 Returns
33 -------
34 Element
35 Created XML element.
36
37 Examples
38 --------
39 >>> elem = createElem('named-E')
40 >>> elem = createElem('literal-E', text='42')
41 >>> elem = createElem('n', text='X', tail='\\n')
42 """
43 node = ET.Element(f'{{{NAMESPACE}}}{tagName}')
44 if text is not None:
45 node.text = text
46 if tail is not None:
47 node.tail = tail
48 if childs is not None:
49 if isinstance(childs, list):
50 node.extend(childs)
51 else:
52 node.append(childs)
53 return node
54
55
56@lru_cache
58 """
59 :param value: expression part to put in a *-E node
60
61 If value is:
62 - a FORTRAN string (python sting containing a ' or a "), returns
63 <f:string-E><f:S>...
64 - a FORTRAN value (python string convertible in real or int, or .FALSE./.TRUE.), returns
65 <f:literal-E><f:l>...
66 - a FORTRAN variable name (pyhon string with only alphanumerical characters and _), returns
67 <named-E/><N><n>...
68 - a FORTRAN operation (other python string), returns the right part of
69 the X affectation statement of the code:
70 "SUBROUTINE T; X=" + value + "; END". The xml is obtained by calling fxtran.
71 """
72
73 # Allowed characters in a FORTRAN variable name
74 allowed = "abcdefghijklmnopqrstuvwxyz"
75 allowed += allowed.upper() + '0123456789_'
76
77 if isint(value) or isfloat(value) or value.upper() in ('.TRUE.', '.FALSE.'):
78 node = createElem('literal-E')
79 node.append(createElem('l', text=str(value)))
80 elif "'" in value or '"' in value:
81 node = createElem('string-E')
82 node.append(createElem('S', text=value))
83 elif all(c in allowed for c in value):
84 nodeN = createElem('N')
85 nodeN.append(createElem('n', text=value))
86 node = createElem('named-E')
87 node.append(nodeN)
88 elif re.match(r'[a-zA-Z_][a-zA-Z0-9_]*%[a-zA-Z_][a-zA-Z0-9_]*$', value):
89 # A%B
90 nodeN = createElem('N')
91 nodeN.append(createElem('n', text=value.split('%')[0]))
92 ct = createElem('ct', text=value.split('%')[1])
93 componentR = createElem('component-R', text='%')
94 componentR.append(ct)
95 nodeRLT = createElem('R-LT')
96 nodeRLT.append(componentR)
97 node = createElem('named-E')
98 node.append(nodeN)
99 node.append(nodeRLT)
100 else:
101 _, xml = fortran2xml(f"SUBROUTINE T; X={value}; END")
102 node = xml.find('.//{*}E-2')[0]
103 return node
104
105
106@debugDecor
107def createExprPart(value, tail=None):
108 """
109 Create an XML node from a FORTRAN expression part.
110
111 Parameters
112 ----------
113 value : str
114 Expression part value to convert.
115 tail : str
116 Tail of the new element
117
118 Returns
119 -------
120 Element
121 XML element representing the expression:
122 - Integer/float: <literal-E><l>value</l></literal-E>
123 - String: <string-E><S>value</S></string-E>
124 - Variable: <named-E><N><n>value</n></N></named-E>
125 - Structure member:
126 <named-E><N><n>A</n></N><R-LT><component-R>%B</component-R></R-LT></named-E>
127 - Expression: parsed via fxtran
128
129 Examples
130 --------
131 >>> createExprPart('42') # Literal
132 >>> createExprPart('X') # Variable
133 >>> createExprPart('A%B') # Structure member
134 """
135 result = copy.deepcopy(_cachedCreateExprPart(value))
136 if tail is not None:
137 result.tail = tail
138 return result
139
140
141@lru_cache
143 """
144 Internal cached function for createExpr.
145
146 Parameters
147 ----------
148 value : str
149 FORTRAN statement(s) to convert.
150
151 Returns
152 -------
153 list
154 List of XML nodes from the statement.
155 """
156 return fortran2xml(f"SUBROUTINE T\n{value}\nEND")[1].find('.//{*}program-unit')[1:-1]
157
158
159@debugDecor
160def createExpr(value):
161 """
162 Convert FORTRAN statements to XML nodes.
163
164 Parameters
165 ----------
166 value : str
167 One or more FORTRAN statements to convert.
168
169 Returns
170 -------
171 list
172 List of XML nodes representing the statements.
173
174 Examples
175 --------
176 >>> nodes = createExpr('X = 42')
177 >>> nodes = createExpr('CALL SUB(X, Y)')
178 >>> nodes = createExpr('IF (A > B) THEN\\n X = 1\\nEND IF')
179 """
180 return copy.deepcopy(_cachedCreateExpr(value))
181
182
183@debugDecor
184def simplifyExpr(expr, add=None, sub=None):
185 """
186 Simplify a numeric expression by combining constants.
187
188 Parameters
189 ----------
190 expr : str
191 Expression to simplify (e.g., '1+I+2+JI-I').
192 add : str, optional
193 Expression to add to the result.
194 sub : str, optional
195 Expression to subtract from the result.
196
197 Returns
198 -------
199 str
200 Simplified expression string.
201
202 Examples
203 --------
204 >>> simplifyExpr('1+1+I+JI-I')
205 '2+JI'
206 >>> simplifyExpr('X+1', add='Y')
207 'X+Y+1'
208
209 Notes
210 -----
211 - Only handles addition and subtraction.
212 - Does not simplify expressions within parentheses.
213 """
214 # We could have used external module, such as sympy, but this routine
215 # (as long as it's sufficient) avoids introducing dependencies.
216 if re.search(r'\‍([^()]*[+-][^()]*\‍)', expr):
217 raise NotImplementedError("Expression cannot (yet) contain + or - sign inside " +
218 f"parenthesis: {expr}")
219
220 def split(expr):
221 """
222 :param s: expression
223 :return: a list of (sign, abs(value))
224 """
225 # splt is ['1', '+', '1', '+', 'I', '+', 'JI', '-', 'I']
226 splt = re.split('([+-])', expr.replace(' ', '').upper())
227 if splt[0] == '':
228 # '-1' returns [
229 splt = splt[1:]
230 if len(splt) % 2 == 1:
231 # expr doesn't start with a sign
232 splt = ['+'] + splt # ['+', '1', '+', '1', '+', 'I', '+', 'JI', '-', 'I']
233 # group sign and operand [('+', '1'), ('+', '1'), ('+', 'I'), ('+', 'JI'), ('-', 'I')]
234 splt = [(splt[2 * i], splt[2 * i + 1]) for i in range(len(splt) // 2)]
235 return splt
236
237 splt = split(expr)
238 if add is not None:
239 splt += split(add)
240 if sub is not None:
241 splt += [('-' if sign == '+' else '+', elem) for (sign, elem) in split(sub)]
242 # Suppress elements with opposite signs
243 for sign, elem in splt.copy():
244 if ('+', elem) in splt and ('-', elem) in splt:
245 splt.remove(('+', elem))
246 splt.remove(('-', elem))
247 # Pre-compute integer additions/substractions
248 found = -1
249 for i, (sign, elem) in enumerate(splt.copy()):
250 if isint(elem):
251 if found == -1:
252 found = i
253 else:
254 result = str((1 if splt[found][0] == '+' else -1) * int(splt[found][1]) +
255 (1 if sign == '+' else -1) * int(elem))
256 splt[found] = split(str(result))[0]
257 splt.pop(i)
258 # Order (no matter what ordering is done but we need to order to allow comparisons)
259 splt.sort(key=''.join)
260 # Empty e.g. '1-1'
261 if len(splt) == 0:
262 splt = [('+', '0')]
263 # Concatenate
264 result = ' '.join(s[0] + ' ' + s[1] for s in splt)
265 if result.startswith('+'):
266 result = result[1:]
267 return result.lstrip(' ')
268
269
270@debugDecor
271def createArrayBounds(lowerBoundstr, upperBoundstr, context):
272 """
273 Return a lower-bound and upper-bound node
274 :param lowerBoundstr: string for the fortran lower bound of an array
275 :param upperBoundstr: string for the fortran upper bound of an array
276 :param context: 'DO' for DO loops
277 'DOCONCURRENT' for DO CONCURRENT loops
278 'ARRAY' for arrays
279 """
280 lowerBound = createElem('lower-bound')
281 lowerBound.insert(0, createExprPart(lowerBoundstr))
282 upperBound = createElem('upper-bound')
283 upperBound.insert(0, createExprPart(upperBoundstr))
284 if context == 'DO':
285 lowerBound.tail = ', '
286 elif context in ('DOCONCURRENT', 'ARRAY'):
287 lowerBound.tail = ':'
288 else:
289 raise PYFTError(f'Context unknown in createArrayBounds: {context}')
290 return lowerBound, upperBound