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):
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
116 Returns
117 -------
118 Element
119 XML element representing the expression:
120 - Integer/float: <literal-E><l>value</l></literal-E>
121 - String: <string-E><S>value</S></string-E>
122 - Variable: <named-E><N><n>value</n></N></named-E>
123 - Structure member:
124 <named-E><N><n>A</n></N><R-LT><component-R>%B</component-R></R-LT></named-E>
125 - Expression: parsed via fxtran
126
127 Examples
128 --------
129 >>> createExprPart('42') # Literal
130 >>> createExprPart('X') # Variable
131 >>> createExprPart('A%B') # Structure member
132 """
133 return copy.deepcopy(_cachedCreateExprPart(value))
134
135
136@lru_cache
138 """
139 Internal cached function for createExpr.
140
141 Parameters
142 ----------
143 value : str
144 FORTRAN statement(s) to convert.
145
146 Returns
147 -------
148 list
149 List of XML nodes from the statement.
150 """
151 return fortran2xml(f"SUBROUTINE T\n{value}\nEND")[1].find('.//{*}program-unit')[1:-1]
152
153
154@debugDecor
155def createExpr(value):
156 """
157 Convert FORTRAN statements to XML nodes.
158
159 Parameters
160 ----------
161 value : str
162 One or more FORTRAN statements to convert.
163
164 Returns
165 -------
166 list
167 List of XML nodes representing the statements.
168
169 Examples
170 --------
171 >>> nodes = createExpr('X = 42')
172 >>> nodes = createExpr('CALL SUB(X, Y)')
173 >>> nodes = createExpr('IF (A > B) THEN\\n X = 1\\nEND IF')
174 """
175 return copy.deepcopy(_cachedCreateExpr(value))
176
177
178@debugDecor
179def simplifyExpr(expr, add=None, sub=None):
180 """
181 Simplify a numeric expression by combining constants.
182
183 Parameters
184 ----------
185 expr : str
186 Expression to simplify (e.g., '1+I+2+JI-I').
187 add : str, optional
188 Expression to add to the result.
189 sub : str, optional
190 Expression to subtract from the result.
191
192 Returns
193 -------
194 str
195 Simplified expression string.
196
197 Examples
198 --------
199 >>> simplifyExpr('1+1+I+JI-I')
200 '2+JI'
201 >>> simplifyExpr('X+1', add='Y')
202 'X+Y+1'
203
204 Notes
205 -----
206 - Only handles addition and subtraction.
207 - Does not simplify expressions within parentheses.
208 """
209 # We could have used external module, such as sympy, but this routine
210 # (as long as it's sufficient) avoids introducing dependencies.
211 if re.search(r'\‍([^()]*[+-][^()]*\‍)', expr):
212 raise NotImplementedError("Expression cannot (yet) contain + or - sign inside " +
213 f"parenthesis: {expr}")
214
215 def split(expr):
216 """
217 :param s: expression
218 :return: a list of (sign, abs(value))
219 """
220 # splt is ['1', '+', '1', '+', 'I', '+', 'JI', '-', 'I']
221 splt = re.split('([+-])', expr.replace(' ', '').upper())
222 if splt[0] == '':
223 # '-1' returns [
224 splt = splt[1:]
225 if len(splt) % 2 == 1:
226 # expr doesn't start with a sign
227 splt = ['+'] + splt # ['+', '1', '+', '1', '+', 'I', '+', 'JI', '-', 'I']
228 # group sign and operand [('+', '1'), ('+', '1'), ('+', 'I'), ('+', 'JI'), ('-', 'I')]
229 splt = [(splt[2 * i], splt[2 * i + 1]) for i in range(len(splt) // 2)]
230 return splt
231
232 splt = split(expr)
233 if add is not None:
234 splt += split(add)
235 if sub is not None:
236 splt += [('-' if sign == '+' else '+', elem) for (sign, elem) in split(sub)]
237 # Suppress elements with opposite signs
238 for sign, elem in splt.copy():
239 if ('+', elem) in splt and ('-', elem) in splt:
240 splt.remove(('+', elem))
241 splt.remove(('-', elem))
242 # Pre-compute integer additions/substractions
243 found = -1
244 for i, (sign, elem) in enumerate(splt.copy()):
245 if isint(elem):
246 if found == -1:
247 found = i
248 else:
249 result = str((1 if splt[found][0] == '+' else -1) * int(splt[found][1]) +
250 (1 if sign == '+' else -1) * int(elem))
251 splt[found] = split(str(result))[0]
252 splt.pop(i)
253 # Order (no matter what ordering is done but we need to order to allow comparisons)
254 splt.sort(key=''.join)
255 # Empty e.g. '1-1'
256 if len(splt) == 0:
257 splt = [('+', '0')]
258 # Concatenate
259 result = ' '.join(s[0] + ' ' + s[1] for s in splt)
260 if result.startswith('+'):
261 result = result[1:]
262 return result.lstrip(' ')
263
264
265@debugDecor
266def createArrayBounds(lowerBoundstr, upperBoundstr, context):
267 """
268 Return a lower-bound and upper-bound node
269 :param lowerBoundstr: string for the fortran lower bound of an array
270 :param upperBoundstr: string for the fortran upper bound of an array
271 :param context: 'DO' for DO loops
272 'DOCONCURRENT' for DO CONCURRENT loops
273 'ARRAY' for arrays
274 """
275 lowerBound = createElem('lower-bound')
276 lowerBound.insert(0, createExprPart(lowerBoundstr))
277 upperBound = createElem('upper-bound')
278 upperBound.insert(0, createExprPart(upperBoundstr))
279 if context == 'DO':
280 lowerBound.tail = ', '
281 elif context in ('DOCONCURRENT', 'ARRAY'):
282 lowerBound.tail = ':'
283 else:
284 raise PYFTError(f'Context unknown in createArrayBounds: {context}')
285 return lowerBound, upperBound