PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
variables.py
1"""
2Variable management and declaration manipulation.
3
4Provides the VarList and Variables classes for managing FORTRAN variable
5declarations and characteristics.
6
7Key Features
8------------
9- Variable declaration parsing and analysis
10- Array specification management (attach/detach dimensions)
11- Automatic array transformation (stack to heap allocation)
12- Unused variable detection and removal
13- Module and dummy argument handling
14
15Classes
16-------
17VarList : Manages a list of variables for a scope
18Variables : Mixin class providing variable manipulation methods
19
20Examples
21--------
22>>> pft = PYFT('input.F90')
23>>> pft.varList.findVar('X')
24{'n': 'X', 't': 'REAL', 'as': [], 'i': None, ...}
25>>> pft.attachArraySpecToEntity()
26>>> pft.modifyAutomaticArrays(
27 declTemplate="{type}, DIMENSION({doubledotshape}), ALLOCATABLE :: {name}",
28 startTemplate="ALLOCATE({name}({shape}))",
29 endTemplate="DEALLOCATE({name})")
30"""
31
32import logging
33import copy
34import re
35from functools import wraps
36import os
37
38from pyfortool.util import PYFTError, debugDecor, alltext, isExecutable, n2name, tag, noParallel
39from pyfortool.expressions import (createArrayBounds, simplifyExpr, createExprPart,
40 createExpr, createElem)
41from pyfortool.tree import updateTree
43
44
45# No @debugDecor for this low-level method
46def _getDeclStmtTag(scopePath):
47 """
48 Get the declaration statement tag for a given scope path.
49
50 Parameters
51 ----------
52 scopePath : str
53 A scope path (e.g., 'module:MOD/sub:SUB' or 'type:MYTYPE').
54
55 Returns
56 -------
57 str
58 The XML tag for declaration statements:
59 - 'component-decl-stmt' for type declarations
60 - 'T-decl-stmt' for other scopes (module, subroutine, function)
61 """
62 if scopePath.split('/')[-1].split(':')[0] == 'type':
63 declStmt = 'component-decl-stmt'
64 else:
65 declStmt = 'T-decl-stmt'
66 return declStmt
67
68
69def updateVarList(func):
70 """
71 Decorator to invalidate the variable list cache.
72
73 Signals that the variable list needs to be recomputed after
74 a modification to the code tree.
75 """
76 @wraps(func)
77 def wrapper(self, *args, **kwargs):
78 result = func(self, *args, **kwargs)
79 self.mainScope._varList = None # pylint: disable=protected-access
80 return result
81 return wrapper
82
83
84class VarList():
85 """
86 Store the characteristics of all variables contained in a source code.
87
88 This class provides methods to query, search, and manage variables
89 declared in a FORTRAN scope (module, subroutine, function, or type).
90
91 Attributes
92 ----------
93 _varList : list
94 List of variable descriptors for the current scope.
95 _fullVarList : list
96 Complete list of variable descriptors for the whole file.
97 _scopePath : str
98 Path of the current scope (e.g., 'module:MODULE/sub:SUB').
99
100 Notes
101 -----
102 - Variables are found in modules only if the 'ONLY' attribute is used.
103 - Array specification and type are unknown for module variables.
104 - 'ASSOCIATE' statements are not followed.
105
106 Examples
107 --------
108 >>> pft = PYFT('myfile.F90')
109 >>> vl = pft.varList
110 >>> vl.findVar('X')
111 {'n': 'X', 't': 'REAL', 'as': [(None, '10')], 'i': None, ...}
112 >>> vl.findVar('Y', array=True) # Search only arrays
113 {'n': 'Y', 't': 'REAL', 'as': [(None, '100'), (None, '100')], ...}
114 """
115 def __init__(self, mainScope, _preCompute=None):
116 """
117 :param mainScope: a PYFT object
118 :param _preCompute: pre-computed values (_varList, _fullVarList, _scopePath)
119
120 Notes: - variables are found in modules only if the 'ONLY' attribute is used
121 - array specification and type is unknown for module variables
122 - 'ASSOCIATE' statements are not followed
123 """
124 # _varList is the effective list of variables for the current scope
125 # _fullVarList is the list of variables of the whole file
126 # We need it because findVar must be able to return a variable declaration
127 # that exists in an upper scope
128 # _scopePath is the path of the current scope
129
130 if _preCompute is None:
131 self._varList = self._fromScope(mainScope)
132 self._scopePath = mainScope.path
133 self._fullVarList = self._varList
134 else:
135 assert mainScope is None
136 self._varList, self._fullVarList, self._scopePath = _preCompute
137
138 def __getitem__(self, *args, **kwargs):
139 return self._varList.__getitem__(*args, **kwargs)
140
141 def __setitem__(self, *args, **kwargs):
142 return self._varList.__setitem__(*args, **kwargs)
143
144 def __delitem__(self, *args, **kwargs):
145 return self._varList.__delitem__(*args, **kwargs)
146
147 def __len__(self, *args, **kwargs):
148 return self._varList.__len__(*args, **kwargs)
149
150 @staticmethod
151 def _fromScope(mainScope):
152 """
153 :param mainScope: a PYFT object
154 """
155 def decodeArraySpecs(arraySpecs):
156 asList = []
157 asxList = []
158 for arraySpec in arraySpecs:
159 lb = arraySpec.find('.//{*}lower-bound')
160 ub = arraySpec.find('.//{*}upper-bound')
161 asList.append([alltext(lb) if lb is not None else None,
162 alltext(ub) if ub is not None else None])
163 asxList.append([lb if lb is not None else None, ub if ub is not None else None])
164 return asList, asxList
165
166 result = []
167 for scope in mainScope.getScopes():
168 # In case scope is a function, we determine the name of the result
169 if tag(scope[0]) == 'function-stmt':
170 rSpec = scope[0].find('./{*}result-spec/{*}N')
171 funcResultName = rSpec if rSpec is not None else \
172 scope[0].find('./{*}function-N/{*}N')
173 funcResultName = n2name(funcResultName).upper()
174 else:
175 funcResultName = ''
176
177 # Find dummy arguments
178 dummyArgs = [n2name(e).upper() for stmt in scope
179 for e in stmt.findall('.//{*}dummy-arg-LT/{*}arg-N/{*}N')]
180
181 # Loop on each declaration statement
182 for declStmt in [stmt for stmt in scope
183 if tag(stmt) in ('T-decl-stmt' 'component-decl-stmt')]:
184 tSpec = alltext(declStmt.find('.//{*}_T-spec_'))
185 iSpec = declStmt.find('.//{*}intent-spec')
186 if iSpec is not None:
187 iSpec = iSpec.text
188 optSpec = False
189 allocatable = False
190 parameter = False
191 pointer = False
192 allattributes = declStmt.findall('.//{*}attribute/{*}attribute-N')
193 for attribute in allattributes:
194 if alltext(attribute).upper() == 'OPTIONAL':
195 optSpec = True
196 if alltext(attribute).upper() == 'ALLOCATABLE':
197 allocatable = True
198 if alltext(attribute).upper() == 'PARAMETER':
199 parameter = True
200 if alltext(attribute).upper() == 'POINTER':
201 pointer = True
202 # Dimensions declared with the DIMENSION attribute
203 arraySpecs = declStmt.findall('.//{*}attribute//{*}array-spec//{*}shape-spec')
204 as0List, asx0List = decodeArraySpecs(arraySpecs)
205
206 # Loop on each declared variables
207 for enDecl in declStmt.findall('.//{*}EN-decl'):
208 varName = n2name(enDecl.find('.//{*}N')).upper()
209 # Dimensions declared after the variable name
210 arraySpecs = enDecl.findall('.//{*}array-spec//{*}shape-spec')
211 asList, asxList = decodeArraySpecs(arraySpecs)
212 # Initial value (parameter or not)
213 init = enDecl.find('./{*}init-E')
214 if init is not None:
215 init = alltext(init)
216
217 argorder = dummyArgs.index(varName) if varName in dummyArgs else None
218 result.append({'as': asList if len(as0List) == 0 else as0List,
219 'asx': asxList if len(asx0List) == 0 else asx0List,
220 'n': varName, 'i': iSpec, 't': tSpec,
221 'arg': varName in dummyArgs,
222 'argorder': argorder,
223 'use': False, 'opt': optSpec, 'allocatable': allocatable,
224 'parameter': parameter, 'pointer': pointer,
225 'result': funcResultName == varName,
226 'init': init, 'scopePath': scope.path})
227
228 # Loop on each use statement
229 for useStmt in [stmt for stmt in scope if tag(stmt) == 'use-stmt']:
230 module = n2name(useStmt.find('.//{*}module-N').find('.//{*}N'))
231 for var in useStmt.findall('.//{*}use-N'):
232 varName = n2name(var.find('.//{*}N'))
233 result.append({'as': None, 'asx': None,
234 'n': varName, 'i': None, 't': None, 'arg': False,
235 'argorder': None,
236 'use': module, 'opt': None, 'allocatable': None,
237 'parameter': None, 'pointer': None, 'result': None,
238 'init': None, 'scopePath': scope.path})
239 return result
240
241 def restrict(self, scopePath, excludeContains):
242 """
243 Return a VarList restricted to a specific scope.
244
245 Parameters
246 ----------
247 scopePath : str
248 Scope path to restrict to (e.g., 'module:MOD/sub:SUB').
249 Use empty string or '/' for root scope.
250 excludeContains : bool
251 If True, exclude variables declared in CONTAINS sections
252 (nested subroutines/functions).
253
254 Returns
255 -------
256 VarList
257 New VarList instance restricted to the specified scope.
258
259 Examples
260 --------
261 >>> vl = pft.varList
262 >>> sub_vl = vl.restrict('module:MOD/sub:SUB', excludeContains=True)
263 >>> sub_vl.findVar('X')
264 {'n': 'X', ...}
265 """
266 scopePath = '' if scopePath == '/' else scopePath
267 root = scopePath + '/' if scopePath == '/' else scopePath
268 varList = [item for item in self._varList
269 if item['scopePath'] == scopePath or
270 (item['scopePath'].startswith(root) and
271 not excludeContains)]
272 return VarList(None, _preCompute=(varList, self._fullVarList, scopePath))
273
274 @debugDecor
275 def findVar(self, varName, array=None, exactScope=False, extraVarList=None):
276 """
277 Search for a variable in the list of declared variables.
278
279 Parameters
280 ----------
281 varName : str
282 Name of the variable to search for.
283 array : bool, optional
284 True to limit search to arrays only,
285 False to limit search to non-array (scalar) variables only,
286 None (default) to return any variable type.
287 exactScope : bool, optional
288 If True, only search for variables declared in the current scope path.
289 If False (default), search in current scope and all parent scopes.
290 extraVarList : list, optional
291 Additional list of variable descriptors to search in.
292 Useful when variables are defined but not yet in varList.
293
294 Returns
295 -------
296 dict or None
297 Variable descriptor dictionary with keys:
298 - 'n': variable name (uppercase)
299 - 't': type specification (e.g., 'REAL', 'INTEGER')
300 - 'as': array specification as list of (lower, upper) tuples
301 - 'i': intent specification (e.g., 'IN', 'OUT', 'INOUT')
302 - 'arg': True if dummy argument
303 - 'scopePath': path where variable is declared
304 - 'use': module name if imported via USE statement
305 Returns None if variable not found.
306
307 Notes
308 -----
309 The function returns the declaration of the variable found in the
310 deepest (most specific) scope. If `array` is specified, only
311 returns the variable if its declaration matches the expected kind.
312
313 Examples
314 --------
315 >>> vl = pft.varList
316 >>> vl.findVar('X') # Find any variable named X
317 {'n': 'X', 't': 'REAL', 'as': [], 'i': None, ...}
318 >>> vl.findVar('Y', array=True) # Find array Y
319 {'n': 'Y', 't': 'REAL', 'as': [(None, '100')], ...}
320 >>> vl.findVar('Z', array=False) # Find scalar Z
321 {'n': 'Z', 't': 'INTEGER', 'as': [], ...}
322 >>> vl.findVar('P', exactScope=True) # Only in current scope
323 {'n': 'P', 't': 'REAL', ...}
324 """
325 extraVarList = extraVarList if extraVarList is not None else []
326 # Select all the variables declared in the current scope or upper,
327 # then select the last declared
328 candidates = {v['scopePath']: v for v in self._fullVarList + extraVarList
329 if v['n'].upper() == varName.upper() and
330 (self._scopePath == v['scopePath'] or
331 (self._scopePath.startswith(v['scopePath'] + '/') and
332 not exactScope))}
333 if len(candidates) > 0:
334 last = candidates[max(candidates, key=len)]
335 if array is True and last.get('as', None) is not None and len(last['as']) > 0:
336 return last
337 if array is False and len(last.get('as', [])) == 0:
338 return last
339 if array is None:
340 return last
341 return None
342
343 @debugDecor
344 def showVarList(self):
345 """
346 Display a formatted list of all variables.
347
348 Prints detailed information about each variable including:
349 - Name and type
350 - Whether it's scalar or array (with dimensions)
351 - Whether it's a dummy argument (with intent) or local variable
352 - Module origin for imported variables
353
354 Examples
355 --------
356 >>> pft = PYFT('input.F90')
357 >>> pft.varList.showVarList()
358 List of variables declared in /module:MOD/sub:SUB:
359 Variable X:
360 is scalar
361 is a dummy argument with intent IN
362 Variable Y:
363 is of rank 2, with dimensions (:,1:10)
364 is a local variable
365 """
366 for sc in set(v['scopePath'] for v in self._varList):
367 print(f'List of variables declared in {sc}:')
368 for var in [var for var in self._varList if var['scopePath'] == sc]:
369 print(f" Variable {var['n']}:")
370 if var['use']:
371 print(f" is a variable taken in the {var['use']} module")
372 else:
373 isscalar = len(var['as']) == 0
374 if isscalar:
375 print(' is scalar')
376 else:
377 print(' is of rank {}, with dimensions {}'.format(len(var['as']),
378 ', '.join([(':'.join([('' if s is None else s)
379 for s in var['as'][i]]))
380 for i in range(len(var['as']))])))
381 if var['arg']:
382 intent = 'without intent' if var['i'] is None else \
383 f"with intent {var['i']}"
384 print(f' is a dummy argument {intent}')
385 else:
386 print(' is a local variable')
387 print()
388
389
390class Variables():
391 """
392 Methods to manage and manipulate FORTRAN variables.
393
394 Provides functionality for declaring, removing, and checking variables
395 in FORTRAN source code.
396 """
397
398 def __init__(self, **kwargs): # pylint: disable=unused-argument
399 """
400 Initialize the Variables handler.
401
402 Parameters
403 ----------
404 **kwargs
405 Keyword arguments for compatibility with parent class initialization.
406 """
407 self._varList = None
408
409 @property
410 def varList(self):
411 """
412 Get the VarList for this scope.
413
414 Returns
415 -------
416 VarList
417 VarList instance containing all variables in the current scope.
418 The list is lazily computed on first access.
419
420 Examples
421 --------
422 >>> pft = PYFT('input.F90')
423 >>> vl = pft.varList
424 >>> vl.findVar('X')
425 {'n': 'X', 't': 'REAL', ...}
426 """
427 # Evaluate the varList object if not already done
428 if self.mainScope._varList is None: # pylint: disable=protected-access
429 self.mainScope._varList = VarList(self.mainScope) # pylint: disable=protected-access
430
431 # Restrict the object to the current node
432 # pylint: disable-next=protected-access
433 return self.mainScope._varList.restrict(self.path, self._excludeContains)
434
435 # No @debugDecor for this low-level method
436 def _normalizeScopeVar(self, scopeVarList):
437 """
438 Internal method to normalize scopeVarList
439 (list of tuples made of scope path, variable name, and optional other values)
440 """
441 return [(self.normalizeScope(scopePath), var.upper(), *other)
442 for (scopePath, var, *other) in scopeVarList]
443
444 # No @debugDecor for this low-level method
445 def _normalizeUniqVar(self, scopeVarList):
446 """
447 Internal method to suppress duplicates in scopeVarList
448 (list of tuples made of scope path, variable name, and optional other values)
449 """
450 # We could use list(set(self._normalizeScopeVar(scopeVarList)))
451 # but order differs from one execution to the other
452 result = []
453 for scopeVar in self._normalizeScopeVar(scopeVarList):
454 if scopeVar not in result:
455 result.append(scopeVar)
456 return result
457
458 @debugDecor
460 """
461 Move DIMENSION attribute from declaration statement to individual entities.
462
463 Finds all type declaration statements (T-decl-stmt) that have a DIMENSION
464 attribute and moves the array specification to each declared variable.
465
466 Transformation
467 -------------
468 Before:
469 REAL, DIMENSION(D%NIJT,D%NKT) :: ZTLK, ZRT
470 INTEGER, PARAMETER, DIMENSION(2) :: IBUEXTRAIND=(/18, 30/)
471
472 After:
473 REAL :: ZTLK(D%NIJT,D%NKT), ZRT(D%NIJT,D%NKT)
474 INTEGER, PARAMETER :: IBUEXTRAIND(2)=(/18, 30/)
475
476 Limitations
477 -----------
478 - DIMENSION attribute must be in uppercase in the source code.
479 - Allocatable arrays (with ':') are not modified.
480 - Variables with existing array specifications are not modified.
481
482 Examples
483 --------
484 >>> pft = PYFT('input.F90')
485 >>> pft.attachArraySpecToEntity()
486 >>> pft.write() # Writes transformed code
487 """
488 # Find all T-decl-stmt elements that have a child element 'attribute'
489 # with attribute-N="DIMENSION"
490 for decl in self.findall('.//{*}T-decl-stmt'):
491 arraySpec = decl.find('./{*}attribute[{*}attribute-N="DIMENSION"]/{*}array-spec')
492 attrElem = decl.find('./{*}attribute[{*}attribute-N="DIMENSION"]/{*}array-spec/...')
493 # Discard allocatable (':' in arraySpec)
494 if arraySpec is not None and ':' not in alltext(arraySpec):
495 # Check if EN-decl elements don't already have an array-spec child element
496 # or an intial value
497 if decl.find('./{*}EN-decl-LT/{*}EN-decl/{*}array-spec') is None and \
498 decl.find('./{*}EN-decl-LT/{*}EN-decl/{*}init-E') is None:
499 # Attach the array-spec element after the EN-N element
500 for elem in decl.findall('./{*}EN-decl-LT/{*}EN-decl'):
501 elem.append(copy.deepcopy(arraySpec))
502 # Remove the dimension and array-spec elements
503 self.removeFromList(attrElem, decl)
504
505 @debugDecor
506 def checkImplicitNone(self, mustRaise=False):
507 """
508 Check for missing IMPLICIT NONE statements in scopes.
509
510 Issues a logging warning if IMPLICIT NONE is not declared in a scope.
511 When mustRaise is True, logs an error and raises PYFTError instead.
512
513 Parameters
514 ----------
515 mustRaise : bool, optional
516 If False (default), issue a warning and continue.
517 If True, issue an error and raise PYFTError.
518
519 Examples
520 --------
521 >>> pft = PYFT('input.F90')
522 >>> pft.checkImplicitNone() # Issues warning if missing
523 >>> pft.checkImplicitNone(mustRaise=True) # Raises error if missing
524 """
525 ok = True
526 for scope in self.getScopes():
527 # The IMPLICIT NONE statement is inherited from the top unit, control at top
528 # unit is enough apart for INTERFACE blocs
529 if (scope.path.count('/') == 0 or
530 (scope.path.count('/') >= 2 and
531 scope.path.split('/')[-2].startswith('interface:'))):
532 if scope.find('./{*}implicit-none-stmt') is None:
533 message = "The 'IMPLICIT NONE' statment is missing in file " + \
534 "'{file}' for {scopePath}.".format(file=scope.getFileName(),
535 scopePath=scope.path)
536 ok = False
537 if mustRaise:
538 logging.error(message)
539 raise PYFTError(message)
540 logging.warning(message)
541 return ok
542
543 @debugDecor
544 def checkIntent(self, mustRaise=False):
545 """
546 Check for missing INTENT attributes on dummy arguments.
547
548 Issues a logging warning for each dummy argument without an INTENT attribute.
549 When mustRaise is True, logs an error and raises PYFTError.
550
551 Parameters
552 ----------
553 mustRaise : bool, optional
554 If False (default), issue warnings and continue.
555 If True, issue errors and raise PYFTError.
556
557 Examples
558 --------
559 >>> pft = PYFT('input.F90')
560 >>> pft.checkIntent() # Issues warning for missing INTENT
561 >>> pft.checkIntent(mustRaise=True) # Raises error
562 """
563 ok = True
564 log = logging.error if mustRaise else logging.warning
565 for var in self.varList:
566 if var['arg'] and var['i'] is None:
567 log(("The dummy argument {} has no INTENT attribute, in " +
568 "file '{}'").format(var['n'], self.getFileName()))
569 ok = False
570 if not ok and mustRaise:
571 raise PYFTError("There are dummy arguments without INTENT attribute in " +
572 "file '{}'".format(self.getFileName()))
573 return ok
574
575 @debugDecor
576 def checkONLY(self, mustRaise=False):
577 """
578 Check for missing ONLY clauses in USE statements.
579
580 Issues a logging warning for each USE statement not followed by an ONLY clause.
581 When mustRaise is True, logs an error and raises PYFTError.
582
583 Parameters
584 ----------
585 mustRaise : bool, optional
586 If False (default), issue warnings and continue.
587 If True, issue errors and raise PYFTError.
588
589 Examples
590 --------
591 >>> pft = PYFT('input.F90')
592 >>> pft.checkONLY() # Issues warning for missing ONLY
593 WARNING: USE MODULE is not followed by an ONLY clause...
594 >>> pft.checkONLY(mustRaise=True) # Raises error
595 """
596 ok = True
597 log = logging.error if mustRaise else logging.warning
598 for useStmt in self.findall('.//{*}use-stmt'):
599 module = useStmt.find('.//{*}module-N')
600 if module.tail is None or module.tail.replace(' ', '').upper() != ',ONLY:':
601 ok = False
602 log(f"USE {n2name(module.find('.//{*}N'))} is not followed by an ONLY clause " +
603 f"in file'{self.getFileName()}'.")
604 if not ok and mustRaise:
605 raise PYFTError("There are USE statements not followed by an ONLY clause " +
606 f"file '{self.getFileName()}'")
607 return ok
608
609 @debugDecor
610 def checkKeyDimConsistency(self, mustRaise=False, stopScopes=None):
611 """
612 Check consistency of MERGE-based array dimensions across scopes.
613
614 For each variable name that has a MERGE-based dimension in any scope,
615 verify that all occurrences of the same variable name use the same
616 MERGE expression (full content inside MERGE()).
617
618 Parameters
619 ----------
620 mustRaise : bool, optional
621 If False (default), issue warnings and continue.
622 If True, issue errors and raise PYFTError.
623 stopScopes : list of str, optional
624 Scope paths to use as roots for call tree filtering.
625 When provided (and the tree is available), only scopes
626 called by these roots are checked.
627
628 Examples
629 --------
630 >>> pft = PYFT('input.F90')
631 >>> pft.checkKeyDimConsistency()
632 >>> pft.checkKeyDimConsistency(mustRaise=True)
633 """
634 ok = True
635 log = logging.error if mustRaise else logging.warning
636 mergeVars = {}
637
638 for scope in self.getScopes():
639 if (stopScopes is not None and
640 self.tree.isValid):
641 if not self.tree.isUnderStopScopes(
642 scope.path, stopScopes, includeStopScopes=True):
643 continue
644 for var in self.varList:
645 if var['as'] is None:
646 continue
647 for dim in var['as']:
648 upper = dim[1]
649 if upper is not None and 'MERGE(' in upper.upper():
650 match = re.match(
651 r'^MERGE\‍((.+)\‍)$', upper.strip(), re.IGNORECASE)
652 if match:
653 mergeContent = match.group(1)
654 varName = var['n'].upper()
655 if varName not in mergeVars:
656 mergeVars[varName] = {}
657 if mergeContent not in mergeVars[varName]:
658 mergeVars[varName][mergeContent] = []
659 mergeVars[varName][mergeContent].append(
660 scope.path)
661
662 for varName, mergeGroups in mergeVars.items():
663 if len(mergeGroups) > 1:
664 for mergeContent, scopes in mergeGroups.items():
665 msg = (f"Variable {varName} has MERGE dimension "
666 f"'MERGE({mergeContent})' in {', '.join(scopes)}.")
667 ok = False
668 log(msg)
669
670 if not ok and mustRaise:
671 raise PYFTError("Inconsistent MERGE-based array dimensions found.")
672 return ok
673
674 @debugDecor
675 @noParallel
676 @updateVarList
677 @updateTree('signal')
678 def removeVar(self, varList, simplify=False):
679 """
680 Remove variables from declarations and argument lists.
681
682 Parameters
683 ----------
684 varList : list of tuple
685 List of variables to remove. Each item is a list or tuple of two elements:
686 - First element: scope path where the variable is used (or declared).
687 This is a '/' separated path where each element has the form:
688 'module:<name>', 'sub:<name>', 'func:<name>', or 'type:<name>'.
689 - Second element: variable name (string).
690
691 Example: [('module:MOD/sub:SUB', 'X'), ('module:MOD/sub:SUB', 'Y')]
692 simplify : bool, optional
693 If True, also remove variables that become unused after the deletion
694 (e.g., kind selectors used only by the removed variable).
695
696 Examples
697 --------
698 Remove variable X from subroutine SUB in module MOD:
699 >>> pft = PYFT('input.F90')
700 >>> pft.removeVar([('module:MOD/sub:SUB', 'X')])
701
702 Remove multiple variables with simplification:
703 >>> pft.removeVar([('module:MOD/sub:SUB', 'KIND_VAR')], simplify=True)
704
705 Notes
706 -----
707 - Dummy arguments are removed from both declarations and argument lists.
708 - USE statement variables are removed from ONLY clauses.
709 - If all variables in a declaration statement are removed, the statement
710 itself is deleted (unless simplify=True, which may delete additional unused variables).
711 """
712 varList = self._normalizeUniqVar(varList)
713
714 # Sort scopes by depth
715 sortedVarList = {}
716 for scopePath, varName in varList:
717 nb = scopePath.count('/')
718 sortedVarList[nb] = sortedVarList.get(nb, []) + [(scopePath, varName.upper())]
719
720 varToRemoveIfUnused = []
721 # Loop on varList starting by inner most variables
722 nbList = [] if len(sortedVarList.keys()) == 0 else \
723 range(max(sortedVarList.keys()) + 1)[::-1]
724 for nb in nbList:
725 sortedVarList[nb] = sortedVarList.get(nb, [])
726 # Loop on scopes
727 for scopePath in list(set(scopePath for scopePath, _ in sortedVarList[nb])):
728 # use of mainScope because variable can be declared upper than self
729 scope = self.mainScope.getScopeNode(scopePath)
730 # Variables searched in this scope
731 varNames = list(set(v for (w, v) in sortedVarList[nb] if w == scopePath))
732 declStmt = _getDeclStmtTag(scopePath)
733 # If scopePath is "module:XX/sub:YY", getScopeNode returns a node
734 # containing the subroutine declaration statements and
735 # excluding the subroutine and functions potentially included
736 # after a "contains" statement
737 previous = None
738 # list() to allow removing during the iteration
739 for node in list(scope):
740 deleted = False
741
742 # Checks if variable is a dummy argument
743 dummyList = node.find('{*}dummy-arg-LT') # This is the list of the dummies
744 if dummyList is not None:
745 # Loop over all dummy arguments
746 for arg in dummyList.findall('.//{*}arg-N'):
747 name = n2name(arg.find('.//{*}N')).upper()
748 for varName in [v for v in varNames if v == name]:
749 # This dummy arg is a searched variable, we remove it from the list
750 scope.removeFromList(arg, dummyList)
751
752 # In case the variable is declared
753 if tag(node) == declStmt:
754 # We are in a declaration statement
755 # list of declaration in the current statment
756 declList = node.find('./{*}EN-decl-LT')
757 for enDecl in declList.findall('.//{*}EN-decl'):
758 name = n2name(enDecl.find('.//{*}N')).upper()
759 for varName in [v for v in varNames if v == name]:
760 # The argument is declared here,
761 # we suppress it from the declaration list
762 varNames.remove(varName)
763 scope.removeFromList(enDecl, declList)
764 # In all the variables are suppressed from the declaration statement
765 if len(list(declList.findall('./{*}EN-decl'))) == 0:
766 if simplify:
767 varToRemoveIfUnused.extend([[scopePath, n2name(nodeN)]
768 for nodeN in node.findall('.//{*}N')])
769 # We will delete the current node but we don't want to lose
770 # any text. So, we put the node's text in the tail of the previous node
771 if previous is not None and node.tail is not None:
772 if previous.tail is None:
773 previous.tail = ''
774 previous.tail += node.tail
775 deleted = True
776 scope.getParent(node).remove(node)
777
778 # In case the variable is a module variable
779 if tag(node) == 'use-stmt':
780 # We are in a use statement
781 useList = node.find('./{*}rename-LT')
782 if useList is not None:
783 for rename in useList.findall('.//{*}rename'):
784 name = n2name(rename.find('.//{*}N')).upper()
785 for varName in [v for v in varNames if v == name]:
786 varNames.remove(varName)
787 # The variable is declared here, we remove it from the list
788 scope.removeFromList(rename, useList)
789 # In case the variable was alone
790 attribute = node.find('{*}module-N').tail
791 if attribute is None:
792 attribute = ''
793 attribute = attribute.replace(' ', '').replace('\n', '')
794 attribute = attribute.replace('&', '').upper()
795 useList = node.find('./{*}rename-LT')
796 if len(useList) == 0 and attribute[0] == ',' and \
797 attribute[1:] == 'ONLY:':
798 # If there is a 'ONLY' attribute,
799 # we suppress the use statement entirely
800 if previous is not None and node.tail is not None:
801 if previous.tail is None:
802 previous.tail = ''
803 previous.tail += node.tail
804 deleted = True
805 scope.getParent(node).remove(node)
806 scope.tree.signal(scope) # Tree must be updated
807 elif len(useList) == 0:
808 # there is no 'ONLY' attribute
809 moduleName = scope.getSiblings(useList, before=True,
810 after=False)[-1]
811 previousTail = moduleName.tail
812 if previousTail is not None:
813 moduleName.tail = previousTail.replace(',', '')
814 scope.getParent(useList).remove(useList)
815 # end loop if all variables have been found
816 if len(varNames) == 0:
817 break
818 # Store node for the following iteration
819 if not deleted:
820 previous = node
821
822 # If some variables have not been found, they are certainly declared one level upper
823 if len(varNames) != 0:
824 newWhere = '/'.join(scopePath.split('/')[:-1])
825 sortedVarList[nb - 1] = sortedVarList.get(nb - 1, []) + \
826 [(newWhere, varName) for varName in varNames]
827
828 if simplify and len(varToRemoveIfUnused) > 0:
829 self.removeVarIfUnused(varToRemoveIfUnused, excludeDummy=True, simplify=True)
830
831 @debugDecor
832 @updateVarList
833 def addVar(self, varList):
834 """
835 Add variables to declarations and argument lists.
836
837 Parameters
838 ----------
839 varList : list of list/tuple
840 List of variable specifications to insert. Each specification is a list
841 of four elements:
842 - Scope path (str): path to the module, subroutine, function, or type
843 where the variable should be declared (e.g., 'module:MOD/sub:SUB').
844 - Variable name (str): name of the variable to add.
845 - Declaration statement (str): FORTRAN declaration (e.g., 'REAL, INTENT(IN) :: X').
846 - Position (int or None): position in dummy argument list for arguments,
847 None for local variables.
848
849 Examples
850 --------
851 Add a local variable:
852 >>> pft = PYFT('input.F90')
853 >>> pft.addVar([('module:MOD/sub:SUB', 'LOCAL_VAR', 'INTEGER :: LOCAL_VAR', None)])
854
855 Add a dummy argument at position 0:
856 >>> pft.addVar([('module:MOD/sub:SUB', 'ARG', 'REAL, INTENT(IN) :: ARG', 0)])
857
858 Add multiple variables:
859 >>> pft.addVar([
860 ... ('module:MOD/sub:SUB', 'X', 'REAL :: X', None),
861 ... ('module:MOD/sub:SUB', 'Y', 'INTEGER :: Y', None)
862 ... ])
863
864 Notes
865 -----
866 - If adding to an argument list, the declaration is automatically updated
867 with the INTENT attribute if specified.
868 - Declaration statements are inserted before the first executable statement.
869 """
870 varList = self._normalizeUniqVar(varList)
871
872 for (scopePath, name, declStmt, pos) in varList:
873 scope = self.getScopeNode(scopePath)
874
875 # Add variable to the argument list
876 if pos is not None:
877 argN = createElem('arg-N')
878 nodeN = createElem('N')
879 nodeN.append(createElem('n', text=name))
880 argN.append(nodeN)
881 # search for a potential node, within the scope, with a list of dummy arguments
882 argLst = scope.find('.//{*}dummy-arg-LT')
883 if argLst is None:
884 # This was a subroutine or function without dummy arguments
885 scope[0][0].tail = '('
886 argLst = createElem('dummy-arg-LT', tail=')')
887 scope[0].insert(1, argLst)
888 scope.insertInList(pos, argN, argLst)
889
890 # Declare the variable
891 # The following test is needed in case several variables are added in the argument list
892 # but the declaration statement is given only once for all the variables
893 if declStmt is not None and declStmt != '':
894 # Declaration statement tag according to path (member of type declaration or not)
895 declStmtTag = _getDeclStmtTag(scopePath)
896
897 if scopePath.split('/')[-1].split(':')[0] == 'type':
898 # Add declaration statement in type declaration
899 # Statement building
900 ds = createExpr(declStmt)[0]
901 previousTail = '\n' + declStmt[:re.search(r'\S', declStmt).start()]
902
903 # node insertion
904 # scope[0] is the T-stmt node, scope[-1] is the end-T-stmt node
905 # scope[-2] is the last node before the end-T-stmt node (last component,
906 # comment or the T-stmt node)
907 ds.tail = scope[-2].tail
908 scope[-2].tail = previousTail
909 scope.insert(-1, ds) # insert before last one
910
911 else:
912 # Add declaration statement (not type declaration case)
913 # Statement building
914 ds = createExpr(declStmt)[0]
915 previousTail = '\n' + declStmt[:re.search(r'\S', declStmt).start()]
916
917 # node insertion index
918 declLst = [node for node in scope if tag(node) == declStmtTag]
919 if len(declLst) != 0:
920 # There already are declaration statements
921 # We look for the last position in the declaration list which do not use
922 # the variable we add
923 for decl in declLst:
924 index = list(scope).index(decl)
925 if name in [n2name(nodeN) for nodeN in decl.findall('.//{*}N')]:
926 break
927 else:
928 # There is no declaration statement
929 # list of executable nodes
930 stmtLst = [node for node in scope if isExecutable(node)]
931 if len(stmtLst) == 0:
932 # There is no executable statement, we insert the declaration at the end
933 # Last node is the ending node (e.g. end-subroutine-stmt)
934 index = len(scope) - 1
935 else:
936 # We insert the declaration just before the first executable statement
937 index = list(scope).index(stmtLst[0])
938
939 # node insertion
940 if index != 0:
941 ds.tail = scope[index - 1].tail
942 scope[index - 1].tail = previousTail
943 scope.insert(index, ds)
944
945 @debugDecor
946 @noParallel
947 @updateVarList
948 @updateTree('signal')
949 def addModuleVar(self, moduleVarList):
950 """
951 Add USE statements for module variables.
952
953 Parameters
954 ----------
955 moduleVarList : list of list/tuple
956 List of module variable specifications. Each specification is a list
957 of three elements:
958 - Scope path (str): path to the location where USE should be added
959 (e.g., 'module:MOD/sub:SUB').
960 - Module name (str): name of the module to USE.
961 - Variable name(s) (str, list, or None):
962 - str: single variable name to import.
963 - list: list of variable names to import.
964 - None: add USE without ONLY clause (import all).
965
966 Examples
967 --------
968 Import single variable Y from MODD_XX into subroutine FOO:
969 >>> pft = PYFT('input.F90')
970 >>> pft.addModuleVar([('sub:FOO', 'MODD_XX', 'Y')])
971 ! Adds: USE MODD_XX, ONLY: Y
972
973 Import multiple variables:
974 >>> pft.addModuleVar([('sub:FOO', 'MODD_XX', ['X', 'Y', 'Z'])])
975 ! Adds: USE MODD_XX, ONLY: X, Y, Z
976
977 Add USE without ONLY (import all):
978 >>> pft.addModuleVar([('sub:FOO', 'MODD_XX', None)])
979 ! Adds: USE MODD_XX
980
981 Import into a module:
982 >>> pft.addModuleVar([('module:MOD', 'OTHER_MOD', 'VAR')])
983
984 Notes
985 -----
986 - Existing USE statements for the same module are updated to include new variables.
987 - Duplicate imports are avoided (variables already imported are not re-added).
988 """
989 moduleVarList = self._normalizeScopeVar(moduleVarList)
990
991 for (scopePath, moduleName, varName) in moduleVarList:
992 if varName is None:
993 varName = []
994 elif not isinstance(varName, list):
995 varName = [varName]
996 scope = self.getScopeNode(scopePath)
997
998 # USE statement already present
999 useLst = [node for node in scope if tag(node) == 'use-stmt']
1000
1001 # Check if we need to add a USE
1002 insertUse = True
1003 for us in useLst:
1004 usName = n2name(us.find('.//{*}module-N//{*}N'))
1005 usVar = [n2name(v.find('.//{*}N')).upper() for v in us.findall('.//{*}use-N')]
1006 if len(varName) == 0 and len(usVar) == 0 and usName.upper() == moduleName.upper():
1007 # There aleardy is a 'USE MODULE' and we wanted to insert a 'USE MODULE'
1008 insertUse = False
1009 elif len(varName) > 0 and len(usVar) > 0 and usName.upper() == moduleName.upper():
1010 # There already is a 'USE MODULE, ONLY:' and we want to insert another
1011 # 'USE MODULE, ONLY:'
1012 # We suppress from the varName list, all the variables already defined
1013 varName = [var for var in varName if var.upper() not in usVar]
1014 if len(varName) == 0:
1015 # All the variables we wanted to import are already defined
1016 insertUse = False
1017
1018 if insertUse:
1019 # Statement building
1020 stmt = f'USE {moduleName}'
1021 if len(varName) > 0:
1022 stmt += ', ONLY:{}'.format(', '.join(varName))
1023 us = createExpr(stmt)[0]
1024
1025 # node insertion index
1026 if len(useLst) != 0:
1027 # There already have use statements, we add the new one after them
1028 index = list(scope).index(useLst[-1]) + 1
1029 else:
1030 # There is no use statement, we add the new node just after the first node
1031 index = 1
1032
1033 us.tail = scope[index - 1].tail
1034 scope[index - 1].tail = '\n'
1035 scope.insert(index, us)
1036 scope.tree.signal(scope) # Tree must be updated
1037
1038 @debugDecor
1039 def showUnusedVar(self):
1040 """
1041 Display unused variables on stdout.
1042
1043 Searches through all scopes and displays variables that are declared
1044 but never used in the code.
1045
1046 Examples
1047 --------
1048 >>> pft = PYFT('input.F90')
1049 >>> pft.showUnusedVar()
1050 Some variables declared in /module:MOD/sub:SUB are unused:
1051 - LOCAL_VAR
1052 - UNUSED_ARRAY
1053 """
1054 scopes = self.getScopes(excludeKinds=['type'])
1055 varUsed = self.isVarUsed([(scope.path, v['n'])
1056 for scope in scopes
1057 for v in self.varList
1058 if v['scopePath'] == scope.path])
1059 for scope in scopes:
1060 varList = [k[1].upper() for (k, v) in varUsed.items() if (not v) and k[0] == scope.path]
1061 if len(varList) != 0:
1062 print(f'Some variables declared in {scope.path} are unused:')
1063 print(' - ' + ('\n - '.join(varList)))
1064
1065 @debugDecor
1066 def checkUnusedLocalVar(self, mustRaise=False, excludeList=None):
1067 """
1068 Check for unused local variables in scopes.
1069
1070 Issues a logging warning for each local variable that is declared but never used.
1071 When mustRaise is True, logs an error and raises PYFTError.
1072
1073 Parameters
1074 ----------
1075 mustRaise : bool, optional
1076 If False (default), issue warnings and continue.
1077 If True, issue errors and raise PYFTError.
1078 excludeList : list of str, optional
1079 List of variable names to exclude from the check.
1080 These variables will not trigger warnings even if unused.
1081
1082 Examples
1083 --------
1084 >>> pft = PYFT('input.F90')
1085 >>> pft.checkUnusedLocalVar() # Issues warnings
1086 WARNING: The LOCAL_VAR variable is not used...
1087 >>> pft.checkUnusedLocalVar(excludeList=['TEMP']) # Exclude TEMP
1088 >>> pft.checkUnusedLocalVar(mustRaise=True) # Raises error
1089 """
1090
1091 if excludeList is None:
1092 excludeList = []
1093 else:
1094 excludeList = [v.upper() for v in excludeList]
1095 scopes = self.getScopes(excludeKinds=['type'])
1096 # We do not check dummy args, module variables
1097 varUsed = self.isVarUsed([(scope.path, v['n'])
1098 for scope in scopes
1099 for v in self.varList
1100 if (v['n'].upper() not in excludeList and
1101 (not v['arg']) and
1102 v['scopePath'].split('/')[-1].split(':')[0] != 'module' and
1103 v['scopePath'] == scope.path)])
1104 ok = True
1105 for scope in scopes:
1106 for var in [k[1].upper() for (k, v) in varUsed.items()
1107 if (not v) and k[0] == scope.path]:
1108 message = f"The {var} variable is not used in file " + \
1109 f"'{scope.getFileName()}' for {scope.path}."
1110 ok = False
1111 if mustRaise:
1112 logging.error(message)
1113 raise PYFTError(message)
1114 logging.warning(message)
1115 return ok
1116
1117 @debugDecor
1118 def removeUnusedLocalVar(self, excludeList=None, simplify=False):
1119 """
1120 Remove unused local variables from declarations.
1121
1122 Removes variables that are declared but never used in the code.
1123 Dummy arguments and module variables are not removed.
1124
1125 Parameters
1126 ----------
1127 excludeList : list of str, optional
1128 List of variable names to exclude from removal.
1129 These variables will be kept even if unused.
1130 simplify : bool, optional
1131 If True, also remove variables that become unused after removal
1132 (e.g., kind selectors).
1133
1134 Examples
1135 --------
1136 >>> pft = PYFT('input.F90')
1137 >>> pft.removeUnusedLocalVar() # Remove all unused locals
1138
1139 Remove unused locals except TEMP and COUNTER:
1140 >>> pft.removeUnusedLocalVar(excludeList=['TEMP', 'COUNTER'])
1141
1142 With simplification (remove cascading unused vars):
1143 >>> pft.removeUnusedLocalVar(simplify=True)
1144
1145 Notes
1146 -----
1147 - Only local variables (declared in SUBROUTINE/FUNCTION scope) are removed.
1148 - Dummy arguments (subroutine parameters) are preserved.
1149 - Variables imported via USE statements are preserved.
1150 """
1151 if excludeList is None:
1152 excludeList = []
1153 else:
1154 excludeList = [item.upper() for item in excludeList]
1155
1156 allVar = [(scope.path, v['n'])
1157 for scope in self.getScopes(excludeKinds=['type'])
1158 for v in scope.varList
1159 if v['n'].upper() not in excludeList and v['scopePath'] == scope.path]
1160 self.removeVarIfUnused(allVar, excludeDummy=True, excludeModule=True, simplify=simplify)
1161
1162 @debugDecor
1163 def addExplicitArrayBounds(self, node=None):
1164 """
1165 Replace implicit array bounds with explicit bounds from declarations.
1166
1167 Transforms array slice notation (e.g., A(:)) into explicit bounds
1168 based on the array's declaration.
1169
1170 Parameters
1171 ----------
1172 node : xml element, optional
1173 Specific XML node to transform. If None (default), transforms
1174 all implicit bounds in all scopes.
1175
1176 Examples
1177 --------
1178 Given declaration: REAL, DIMENSION(1:10) :: A, B
1179 And usage: A(:) = B(:)
1180
1181 After transformation:
1182 A(1:10) = B(1:10)
1183
1184 Notes
1185 -----
1186 - Only handles 1D array slices (A(:) notation).
1187 - Does not modify allocatable arrays (where ':' is part of declaration).
1188 - Does not modify character type arrays.
1189 """
1190 if node is None:
1191 nodes = [(scope, scope) for scope in self.getScopes()]
1192 else:
1193 nodes = [(self, node)]
1194
1195 for (scope, childNode) in nodes:
1196 for parent4 in [parent4 for parent4
1197 in childNode.findall('.//{*}section-subscript/../../../..')
1198 if parent4.find('./{*}R-LT/{*}component-R') is None]:
1199 # Shape of type members is unknown
1200 for parent in parent4.findall('.//{*}section-subscript/..'):
1201 for sub in parent.findall('.//{*}section-subscript'):
1202 lowerUsed = sub.find('./{*}lower-bound')
1203 upperUsed = sub.find('./{*}upper-bound')
1204 # A slice can be A(:), A(I:) or A(:I), but not A(I)
1205 # With A(:) and A(:I), lowerUsed is None
1206 # With A(I:) lowerUsed.tail contains a ':'
1207 # With A(I) lowerUsed.tail doesn't contain a ':'
1208 if lowerUsed is None or \
1209 (lowerUsed.tail is not None and ':' in lowerUsed.tail):
1210 if lowerUsed is None or upperUsed is None:
1211 # At least one array bound is implicit
1212 varDesc = scope.varList.findVar(n2name(parent4.find('.//{*}N')))
1213 if varDesc is not None and varDesc['t'] is not None and \
1214 'CHAR' not in varDesc['t']: # module array or character
1215 lowerDecl, upperDecl = varDesc['as'][list(parent).index(sub)]
1216 if lowerDecl is None:
1217 lowerDecl = '1'
1218 # When a bound is explicit, we keep it, otherwise we
1219 # take the declared bound
1220 lowerBound = lowerDecl if lowerUsed is None \
1221 else alltext(lowerUsed)
1222 upperBound = upperDecl if upperUsed is None \
1223 else alltext(upperUsed)
1224 if upperBound is not None: # case of implicit shape
1225 lowerXml, upperXml = createArrayBounds(lowerBound,
1226 upperBound,
1227 'ARRAY')
1228 # We remove current explicit bounds or the ':',
1229 # and replace them by the new explicit declaration
1230 for nnn in sub:
1231 if tag(nnn) in ('lower-bound', 'upper-bound'):
1232 sub.remove(nnn)
1233 else:
1234 raise PYFTError("Unexpected case, " +
1235 "tag is {}".format(tag(nnn)))
1236 sub.text = '' # Delete the initial ':'
1237 sub.extend([lowerXml, upperXml])
1238
1239 @debugDecor
1240 @noParallel
1242 """
1243 Add explicit array parentheses to array variables.
1244
1245 Transforms array variable references to include explicit slice notation.
1246 For example, A becomes A(:) when A is declared as an array.
1247
1248 Examples
1249 --------
1250 Given declaration: REAL, DIMENSION(10) :: A, B, C
1251 And usage: A = B + C
1252
1253 After transformation:
1254 A(:) = B(:) + C(:)
1255
1256 Notes
1257 -----
1258 - Only modifies known arrays (declared with dimensions).
1259 - Excludes arrays in ALLOCATED, ASSOCIATED, and PRESENT intrinsic calls.
1260 - Does not modify pointer/allocatable arrays in call statements
1261 (to maintain interface compatibility).
1262 """
1263 # Loop on scopes
1264 for scope in self.getScopes():
1265 for node in scope.iter():
1266 # Arrays are used in statements
1267 # * We must exclude allocate-stmt, deallocate-stmt, pointer-a-stmt, T-decl-stmt and
1268 # associate-stmt,
1269 # nullify-stmt function-stmt, subroutine-stmt, interface-stmt must be kept
1270 # untouched
1271 # action-stmt is discarded as it contains another statement.
1272 # * Array variables to modify can be found in
1273 # - simple affectation (a-stmt),
1274 # - if construct conditions (if-then-stmt, else-if-stmt),
1275 # - where-construct mask (where-construct-stmt, else-where-stmt),
1276 # - if statement condition (if-stmt/condition-E, be carefull to not take the
1277 # whole if-stmt as it contains the action-stmt which,
1278 # in turn, can contain an allocate/deallocate/pointer
1279 # assignment)
1280 # - where statement mask (where-stmt/mask-E and not directly
1281 # where-stmt as for if-stmt),
1282 # - select-case-stmt and case-stmt,
1283 # - do-stmt or forall-construct-stmt (e.g. FOR I=LBOUND(A, 0), UBOUND(A, 0))
1284 # - forall-stmt/forall-triplet-spec-LT
1285 nodeToTransform = None
1286 if tag(node) in ('allocate-stmt', 'deallocate-stmt', 'pointer-a-stmt',
1287 'T-decl-stmt', 'associate-stmt', 'function-stmt',
1288 'subroutine-stmt', 'interface-stmt', 'action-stmt',
1289 'nullify-stmt'):
1290 # excluded
1291 pass
1292 elif tag(node) in ('if-stmt', 'where-stmt', 'forall-stmt'):
1293 # We must transform only a part of the node
1294 part = {'if-stmt': 'condition-E', 'where-stmt': 'mask-E',
1295 'forall-stmt': 'forall-triplet-spec-LT'}[tag(node)]
1296 nodeToTransform = node.find('./{*}' + part)
1297 elif tag(node).endswith('-stmt'):
1298 nodeToTransform = node
1299 if nodeToTransform is not None:
1300 scope.addArrayParenthesesInNode(nodeToTransform)
1301
1302 @debugDecor
1304 """
1305 Add explicit array parentheses to arrays within a specific XML node.
1306
1307 Parameters
1308 ----------
1309 node : xml element
1310 XML node in which to add array parentheses.
1311 Only processes named-E elements within this node.
1312
1313 See Also
1314 --------
1315 addArrayParentheses : Add parentheses in all scopes.
1316
1317 Examples
1318 --------
1319 >>> pft = PYFT('input.F90')
1320 >>> node = pft.find('.//{*}a-stmt')
1321 >>> pft.addArrayParenthesesInNode(node)
1322 """
1323 # Loop on variables
1324 for namedE in node.findall('.//{*}named-E'):
1325 if not namedE.find('./{*}R-LT'): # no parentheses
1326 if not self.isNodeInProcedure(namedE, ('ALLOCATED', 'ASSOCIATED', 'PRESENT')):
1327 # Pointer/allocatable used in ALLOCATED/ASSOCIATED must not be modified
1328 # Array in present must not be modified
1329 nodeN = namedE.find('./{*}N')
1330 var = self.varList.findVar(n2name(nodeN))
1331 if var is not None and var['as'] is not None and len(var['as']) > 0 and \
1332 not ((var['pointer'] or var['allocatable']) and self.isNodeInCall(namedE)):
1333 # This is a known array variable, with no parentheses
1334 # But we exclude pointer allocatable in call statement because the called
1335 # subroutine can wait for a pointer/allocatable and not an array (and it
1336 # is difficult to guess as it would need to find the source code of the
1337 # subroutine)
1338 nodeRLT = createElem('R-LT')
1339 namedE.insert(list(namedE).index(nodeN) + 1, nodeRLT)
1340 arrayR = createElem('array-R', text='(')
1341 nodeRLT.append(arrayR)
1342 sectionSubscriptLT = createElem('section-subscript-LT', tail=')')
1343 arrayR.append(sectionSubscriptLT)
1344 for _ in var['as']:
1345 sectionSubscript = createElem('section-subscript', text=':', tail=', ')
1346 sectionSubscriptLT.append(sectionSubscript)
1347 sectionSubscript.tail = None # last one
1348
1349 @debugDecor
1351 """
1352 Remove array parentheses if no index selection is needed.
1353
1354 When an array has full slice notation (e.g., A(:,:)) that matches
1355 the entire array, removes the parentheses.
1356
1357 Parameters
1358 ----------
1359 node : xml element
1360 XML node in which to remove unnecessary parentheses.
1361
1362 Transformation
1363 -------------
1364 Before: A(:,:) (when A is declared as 2D with full bounds)
1365 After: A
1366
1367 Notes
1368 -----
1369 - Only removes parentheses when all dimensions use full slice (:) notation.
1370 - Preserves parentheses if any dimension has explicit index selection.
1371 - Excludes arrays in ALLOCATED, ASSOCIATED, and PRESENT intrinsic calls.
1372 """
1373 # Loop on variables
1374 for namedE in node.findall('.//{*}named-E'):
1375 if namedE.find('./{*}R-LT'): # parentheses
1376 if not self.isNodeInProcedure(namedE, ('ALLOCATED', 'ASSOCIATED', 'PRESENT')):
1377 # Pointer/allocatable used in ALLOCATED/ASSOCIATED must not be modified
1378 # Array in present must not be modified
1379 nodeN = namedE.find('./{*}N')
1380 var = self.varList.findVar(n2name(nodeN))
1381 if var is not None and var['as'] is not None and len(var['as']) > 0 and \
1382 not ((var['pointer'] or var['allocatable']) and self.isNodeInCall(namedE)):
1383 arrayR = namedE.findall('./{*}R-LT')
1384 for arr in arrayR:
1385 sectionSubscriptLT = arr.findall('.//{*}section-subscript-LT')
1386 for ss in sectionSubscriptLT:
1387 lowerBound = ss.findall('.//{*}lower-bound')
1388 if len(lowerBound) == 0:
1389 # Node to be removed <f:R-LT><f:array-R><f:section-subscript-LT>
1390 par = self.getParent(ss, level=2)
1391 parOfpar = self.getParent(par)
1392 parOfpar.remove(par)
1393
1394 @debugDecor
1395 @updateVarList
1396 def modifyAutomaticArrays(self, declTemplate=None, startTemplate=None, endTemplate=None):
1397 """
1398 Transform automatic arrays using customizable templates.
1399
1400 Modifies automatic array declarations in subroutines and functions by
1401 applying templates for declaration, initialization, and cleanup.
1402
1403 Parameters
1404 ----------
1405 declTemplate : str, optional
1406 Template for the array declaration. If None, declaration is unchanged.
1407 startTemplate : str, optional
1408 Template for code to insert as the first executable statement
1409 (e.g., allocation).
1410 endTemplate : str, optional
1411 Template for code to insert as the last executable statement
1412 (e.g., deallocation).
1413
1414 Returns
1415 -------
1416 int
1417 Number of arrays modified.
1418
1419 Placeholders
1420 ------------
1421 Each template can use the following placeholders (case-sensitive):
1422
1423 ============ ==================================================
1424 Placeholder Description (for declaration "A(I, I:J, 0:I)")
1425 ============ ==================================================
1426 {name} Variable name (e.g., "A")
1427 {type} Type specification (e.g., "REAL")
1428 {doubledotshape} Colon-separated dimensions (e.g., ":, :, :")
1429 {shape} Bounds with indices (e.g., "I, I:J, 0:I")
1430 {lowUpList} Flattened bounds (e.g., "1, I, I, J, 0, I")
1431 ============ ==================================================
1432
1433 Examples
1434 --------
1435 Transform automatic arrays to allocatables:
1436
1437 >>> pft = PYFT('input.F90')
1438 >>> pft.modifyAutomaticArrays(
1439 ... declTemplate="{type}, DIMENSION({doubledotshape}), ALLOCATABLE :: {name}",
1440 ... startTemplate="ALLOCATE({name}({shape}))",
1441 ... endTemplate="DEALLOCATE({name})"
1442 ... )
1443
1444 Given input:
1445 REAL, DIMENSION(I, I:J, 0:I) :: A
1446 A = 1.0
1447
1448 Produces:
1449 REAL, DIMENSION(:,:,:), ALLOCATABLE :: A
1450 ALLOCATE(A(I, I:J, 0:I))
1451 A = 1.0
1452 DEALLOCATE(A)
1453
1454 Notes
1455 -----
1456 - Only processes automatic arrays (stack-allocated based on parameters).
1457 - Excludes dummy arguments, allocatable, pointer, and function result arrays.
1458 - Arrays with initial values are not processed.
1459 - Variables are processed in dependency order to handle interdependencies.
1460 """
1461 templates = {'decl': declTemplate if declTemplate is not None else '',
1462 'start': startTemplate if startTemplate is not None else '',
1463 'end': endTemplate if endTemplate is not None else ''} # ordered dict
1464
1465 number = 0
1466 for scope in [scope for scope in self.getScopes()
1467 if scope.path.split('/')[-1].split(':')[0] in ('sub', 'func')]:
1468 # For all subroutine and function scopes
1469 # Determine the list of variables to transform
1470 varListToTransform = []
1471 for var in [var for var in scope.varList
1472 if var['as'] is not None and
1473 len(var['as']) > 0 and
1474 'CHARACTER' not in var['t'] and
1475 not (var['arg'] or var['allocatable'] or
1476 var['pointer'] or var['result'])]:
1477 # For all automatic arrays, which are not argument, not allocatable,
1478 # not pointer and not result
1479 if var['init'] is None:
1480 # Array with initial value can't be processed by modifyAutomaticArrays
1481 varListToTransform.append(var)
1482 # A variable can make use of the size of another variable in its declaration statement
1483 # We order the variables to not insert the declaration of a variable before the
1484 # declaration of the variables it depends on
1485 orderedVarListToTransform = []
1486 while len(varListToTransform) > 0:
1487 nAdded = 0
1488 for var in varListToTransform[:]:
1489 # flatten var['asx'] excluding None
1490 listN = [x for dim in var['asx'] for x in dim if x is not None]
1491 listN = [n2name(nodeN).upper() for asx in listN
1492 for nodeN in asx.findall('.//{*}N/{*}n/..')]
1493 if len(set(listN).intersection([v['n'].upper()
1494 for v in varListToTransform])) == 0:
1495 # Variable var does not use variables still in varListToTransform
1496 varListToTransform.remove(var)
1497 orderedVarListToTransform.append(var)
1498 nAdded += 1
1499 if nAdded == 0:
1500 raise PYFTError('It seems that there is a circular reference in ' +
1501 'the declaration statements')
1502 # Loop on variable to transform
1503 for var in orderedVarListToTransform[::-1]: # reverse order
1504 number += 1
1505 # Apply the template
1506 templ = copy.deepcopy(templates)
1507 for templPart in templ:
1508 if '{doubledotshape}' in templ[templPart]:
1509 templ[templPart] = templ[templPart].replace(
1510 '{doubledotshape}', ','.join([':'] * len(var['as'])))
1511 if '{shape}' in templ[templPart]:
1512 result = []
1513 for i in range(len(var['as'])):
1514 if var['as'][i][0] is None:
1515 result.append(var['as'][i][1])
1516 else:
1517 result.append(var['as'][i][0] + ':' + var['as'][i][1])
1518 templ[templPart] = templ[templPart].replace('{shape}', ', '.join(result))
1519 if '{name}' in templ[templPart]:
1520 templ[templPart] = templ[templPart].replace('{name}', var['n'])
1521 if '{type}' in templ[templPart]:
1522 templ[templPart] = templ[templPart].replace('{type}', var['t'])
1523 if '{lowUpList}' in templ[templPart]:
1524 result = []
1525 for i in range(len(var['as'])):
1526 if var['as'][i][0] is None:
1527 result.extend([1, var['as'][i][1]])
1528 else:
1529 result.extend([var['as'][i][0], var['as'][i][1]])
1530 templ[templPart] = templ[templPart].replace(
1531 '{lowUpList}', ', '.join([str(r) for r in result]))
1532
1533 # Get the xml for the template
1534 separator = "!ABCDEFGHIJKLMNOPQRSTUVWabcdefghijklmnopqrstuvwxyz0123456789"
1535 part = 0
1536 for node in createExpr(templ['decl'] + '\n' + separator + '\n' +
1537 templ['start'] + '\n' + separator + '\n' +
1538 templ['end']):
1539 templPart = list(templ.keys())[part]
1540 if not isinstance(templ[templPart], list):
1541 templ[templPart] = []
1542 if tag(node) == 'C' and node.text == separator:
1543 part += 1
1544 else:
1545 templ[templPart].append(node)
1546
1547 # Replace declaration statement
1548 # We look for the last position in the declaration list which do not use the
1549 # variable we add
1550 # This algorithm will stop when the original declaration statement is encoutered
1551 for decl in scope.findall('./{*}T-decl-stmt'):
1552 index = list(scope).index(decl)
1553 if var['n'] in [n2name(nodeN) for nodeN in decl.findall('.//{*}N')]:
1554 break
1555 scope.removeVar([(scope.path, var['n'])], simplify=False)
1556 for nnn in templ['decl'][::-1]:
1557 scope.insert(index, nnn)
1558
1559 # Insert statements
1560 for nnn in templ['start'][::-1]:
1561 scope.insertStatement(nnn, True)
1562 for nnn in templ['end'][::-1]:
1563 scope.insertStatement(nnn, False)
1564 return number
1565
1566 @staticmethod
1567 @debugDecor
1568 def varSpec2stmt(varSpec, implicitDeclaration=False):
1569 """
1570 :param varSpec: a variable description, same form as the items return by self.varList
1571 :param implicitDeclaration: True if the variable may contain implicit declaration
1572 (e.g. outside of PHYEX)
1573 :return: the associated declarative statement
1574 """
1575 if varSpec['use'] is not False:
1576 stmt = f"USE {varSpec['use']}, ONLY: {varSpec['n']}"
1577 else:
1578 stmt = varSpec['t']
1579 if varSpec['as']:
1580 stmt += ', DIMENSION('
1581 dl = []
1582 for el in varSpec['as']:
1583 if el[0] is None and el[1] is None:
1584 dl.append(':')
1585 elif el[0] is None:
1586 dl.append(el[1])
1587 else:
1588 dl.append(el[0] + ':' + el[1])
1589 if (varSpec['allocatable'] and not all(d == ':' for d in dl)) or \
1590 (any(d == ':' for d in dl) and not varSpec['allocatable']):
1591 if not implicitDeclaration:
1592 raise PYFTError('Missing dim are mandatory and allowed ' +
1593 'only for allocatable arrays')
1594 stmt += ', '.join(dl) + ')'
1595 if varSpec['allocatable']:
1596 stmt += ", ALLOCATABLE"
1597 if varSpec['parameter']:
1598 stmt += ", PARAMETER"
1599 if varSpec['i'] is not None:
1600 stmt += ", INTENT(" + varSpec['i'] + ")"
1601 if varSpec['opt'] is True:
1602 stmt += ", OPTIONAL"
1603 stmt += " :: " + varSpec['n']
1604 if varSpec['init'] is not None:
1605 stmt += "=" + varSpec['init']
1606 return stmt
1607
1608 @debugDecor
1609 def findIndexArrayBounds(self, arr, index, loopVar):
1610 """
1611 Find bounds and loop variable for a given array index
1612 :param arr: array node (named-E node with a array-R child)
1613 :param index: index of the rank of the array
1614 :param loopVar: None to create new variable for each added DO loop
1615 or a function that return the name of the variable to use for the
1616 loop control.
1617 This function returns a string (name of the variable), or True to create
1618 a new variable, or False to not transform this statement
1619 The functions takes as arguments:
1620 - lower and upper bounds as defined in the declaration statement
1621 - lower and upper bounds as given in the statement
1622 - name of the array
1623 - index of the rank
1624 :return: the tuple (loopName, lowerBound, upperBound) where:
1625 loopName is the name of the variable to use for the loop
1626 lower and upper bounds are the bounds to use for the DO loop
1627 loopName can be:
1628 - a string
1629 - False to discard this index
1630 - True to create a new variable to loop with
1631 """
1632 name = n2name(arr.find('./{*}N'))
1633 ss = arr.findall('./{*}R-LT/{*}array-R/{*}section-subscript-LT/{*}section-subscript')[index]
1634 # We are only interested by the subscript containing ':'
1635 # we must not iterate over the others, eg: X(:,1)
1636 if ':' in alltext(ss):
1637 # Look for lower and upper bounds for iteration and declaration
1638 lowerUsed = ss.find('./{*}lower-bound')
1639 upperUsed = ss.find('./{*}upper-bound')
1640 varDesc = self.varList.findVar(name, array=True)
1641 if varDesc is not None:
1642 lowerDecl, upperDecl = varDesc['as'][index]
1643 if lowerDecl is None:
1644 lowerDecl = '1' # default lower index for FORTRAN arrays
1645 else:
1646 lowerDecl, upperDecl = None, None
1647
1648 # name of the loop variable
1649 if loopVar is None:
1650 # loopVar is not defined, we create a new variable for the loop only if lower
1651 # and upper bounds have been found (easy way to discard character strings)
1652 varName = lowerDecl is not None and upperDecl is not None
1653 else:
1654 varName = loopVar(lowerDecl, upperDecl,
1655 None if lowerUsed is None else alltext(lowerUsed),
1656 None if upperUsed is None else alltext(upperUsed), name, index)
1657 return (varName,
1658 lowerDecl if lowerUsed is None else alltext(lowerUsed),
1659 upperDecl if upperUsed is None else alltext(upperUsed))
1660 return None
1661
1662 @debugDecor
1663 def arrayR2parensR(self, namedE, table):
1664 """
1665 Transform a array-R into a parens-R node by replacing slices by variables
1666 In 'A(:)', the ':' is in a array-R node whereas in 'A(JL)', 'JL' is in a parens-R node.
1667 Both the array-R and the parens-R nodes are inside a R-LT node
1668 :param namedE: a named-E node
1669 :param table: dictionnary returned by the decode function
1670 :param varList: None or a VarList object in which varaibles are searched for
1671 """
1672 # Before A(:): <f:named-E>
1673 # <f:N><f:n>A</f:n></f:N>
1674 # <f:R-LT>
1675 # <f:array-R>(
1676 # <f:section-subscript-LT>
1677 # <f:section-subscript>:</f:section-subscript>
1678 # </f:section-subscript-LT>)
1679 # </f:array-R>
1680 # </f:R-LT>
1681 # </f:named-E>
1682 # After A(I): <f:named-E>
1683 # <f:N><f:n>A</f:n></f:N>
1684 # <f:R-LT>
1685 # <f:parens-R>(
1686 # <f:element-LT>
1687 # <f:element><f:named-E><f:N><f:n>I</f:n></f:N></f:named-E></f:element>
1688 # </f:element-LT>)
1689 # </f:parens-R>
1690 # </f:R-LT>
1691 # </f:named-E>
1692
1693 nodeRLT = namedE.find('./{*}R-LT')
1694 arrayR = nodeRLT.find('./{*}array-R') # Not always in first position, eg: ICED%XRTMIN(:)
1695 if arrayR is not None:
1696 index = list(nodeRLT).index(arrayR)
1697 parensR = createElem('parens-R', text='(', tail=')')
1698 elementLT = createElem('element-LT')
1699 parensR.append(elementLT)
1700 ivar = -1
1701 for ss in nodeRLT[index].findall('./{*}section-subscript-LT/{*}section-subscript'):
1702 element = createElem('element', tail=', ')
1703 elementLT.append(element)
1704 if ':' in alltext(ss):
1705 ivar += 1
1706 varName = list(table.keys())[ivar] # variable name
1707 lower = ss.find('./{*}lower-bound')
1708 upper = ss.find('./{*}upper-bound')
1709 if lower is not None:
1710 lower = alltext(lower)
1711 if upper is not None:
1712 upper = alltext(upper)
1713 if lower is not None and ss.text is not None and ':' in ss.text:
1714 # fxtran bug workaround
1715 upper = lower
1716 lower = None
1717 if lower is None and upper is None:
1718 # E.g. 'A(:)'
1719 # In this case we use the DO loop bounds without checking validity with
1720 # respect to the array declared bounds
1721 element.append(createExprPart(varName))
1722 else:
1723 # E.g.:
1724 # !$mnh_expand_array(JI=2:15)
1725 # A(2:15) or A(:15) or A(2:)
1726 if lower is None:
1727 # lower bound not defined, getting lower declared bound for this array
1728 lower = self.varList.findVar(n2name(namedE.find('{*}N')),
1729 array=True)['as'][ivar][0]
1730 if lower is None:
1731 lower = '1' # default fortran lower bound
1732 elif upper is None:
1733 # upper bound not defined, getting upper declared bound for this array
1734 upper = self.varList.findVar(n2name(namedE.find('{*}N')),
1735 array=True)['as'][ivar][1]
1736 # If the DO loop starts from JI=I1 and goes to JI=I2; and array
1737 # bounds are J1:J2
1738 # We compute J1-I1+JI and J2-I2+JI and they should be the same
1739 # E.g: array bounds could be 'I1:I2' (becoming JI:JI) or 'I1+1:I2+1"
1740 # (becoming JI+1:JI+1)
1741 newlower = simplifyExpr(lower, add=varName, sub=table[varName][0])
1742 newupper = simplifyExpr(upper, add=varName, sub=table[varName][1])
1743 if newlower != newupper:
1744 raise PYFTError(("Don't know how to do with an array declared with " +
1745 "'{la}:{ua}' and a loop from '{ll}' to '{ul}'"
1746 ).format(la=lower, ua=upper,
1747 ll=table[varName][0],
1748 ul=table[varName][1]))
1749 element.append(createExprPart(newlower))
1750 else:
1751 element.append(ss.find('./{*}lower-bound'))
1752 element.tail = None # last element
1753 nodeRLT.remove(nodeRLT[index])
1754 nodeRLT.insert(index, parensR)
1755
1756 @debugDecor
1757 def findArrayBounds(self, arr, loopVar, extraVarList=None):
1758 """
1759 Find bounds and loop variable given an array
1760 :param arr: array node (named-E node with a array-R child)
1761 :param loopVar: None to create new variable for each added DO loop
1762 or a function that return the name of the variable to use for the loop
1763 control.
1764 This function returns a string (name of the variable), or True to create
1765 a new variable, or False to not transform this statement
1766 The functions takes as arguments:
1767 - lower and upper bounds as defined in the declaration statement
1768 - lower and upper bounds as given in the statement
1769 - name of the array
1770 - index of the rank
1771 :param extraVarList: None or list of variables (such as those contained in a VarList object)
1772 defined but not yet available in the self.varList object.
1773 :return: the tuple (table, newVar) where:
1774 table is a dictionnary: keys are loop variable names
1775 values are tuples with lower and upper bounds
1776 newVar is a list of loop variables not found in varList. This list has the same
1777 format as the varList list.
1778
1779 In case the loop variable cannot be defined, the function returns (None, [])
1780 """
1781 table = {} # ordered since python 3.7
1782 name = n2name(arr.find('./{*}N'))
1783 varNew = []
1784 extraVarList = extraVarList if extraVarList is not None else []
1785
1786 # Iteration on the different subscript
1787 for iss, ss in enumerate(arr.findall('./{*}R-LT/{*}array-R/{*}section-subscript-LT/' +
1788 '{*}section-subscript')):
1789 # We are only interested by the subscript containing ':'
1790 # we must not iterate over the others, eg: X(:,1)
1791 if ':' in alltext(ss):
1792 # Look for loop variable name and lower/upper bounds for iteration
1793 varName, lower, upper = self.findIndexArrayBounds(arr, iss, loopVar)
1794 # varName can be a string (name to use), True (to create a variable),
1795 # False (to discard the array
1796 if varName is not False and varName in table:
1797 raise PYFTError(("The variable {var} must be used for the rank #{i1} whereas " +
1798 "it is already used for rank #{i2} (for array {name})."
1799 ).format(var=varName, i1=str(iss),
1800 i2=str(list(table.keys()).index(varName)),
1801 name=name))
1802 if varName is True:
1803 # A new variable must be created
1804 # We look for a variable name that don't already exist
1805 # We can reuse a newly created varaible only if it is not used for the previous
1806 # indexes of the same statement
1807 j = 0
1808 found = False
1809 while not found:
1810 j += 1
1811 varName = 'J' + str(j)
1812 var = self.varList.findVar(varName, extraVarList=extraVarList + varNew)
1813 if (var is None or var.get('new', False)) and varName not in table:
1814 found = True
1815 varDesc = {'as': [], 'asx': [], 'n': varName, 'i': None,
1816 't': 'INTEGER', 'arg': False, 'use': False, 'opt': False,
1817 'scopePath': self.path}
1818 if varDesc not in varNew:
1819 varNew.append(varDesc)
1820
1821 elif (varName is not False and
1822 self.varList.findVar(varName, array=False, exactScope=True) is None):
1823 # We must declare the variable
1824 varDesc = {'as': [], 'asx': [], 'n': varName, 'i': None,
1825 't': 'INTEGER', 'arg': False, 'use': False, 'opt': False,
1826 'scopePath': self.path}
1827 varNew.append(varDesc)
1828
1829 # fill table
1830 table[varName] = (lower, upper)
1831
1832 return (None, []) if False in table else (table, varNew)
1833
1834 @debugDecor
1835 @updateVarList
1836 def renameVar(self, oldName, newName):
1837 """
1838 :param oldName: old name of the variable
1839 :param newName: new name of the variable
1840 """
1841 for node in self.findall('.//{*}N'):
1842 if n2name(node).upper() == oldName.upper():
1843 # Remove all n tag but one
1844 for nnn in node.findall('./{*}n')[1:]:
1845 node.remove(nnn)
1846 # Fill the first n with the new name
1847 node.find('./{*}n').text = newName
1848
1849 @debugDecor
1850 def removeVarIfUnused(self, varList, excludeDummy=False, excludeModule=False, simplify=False):
1851 """
1852 :param varList: list of variables to remove if unused. Each item is a list or tuple of two
1853 elements.
1854 The first one describes where the variable is used, the second one is
1855 the name of the variable. The first element is a '/'-separated path with
1856 each element having the form 'module:<name of the module>',
1857 'sub:<name of the subroutine>' or 'func:<name of the function>'
1858 :param excludeDummy: if True, dummy arguments are always kept untouched
1859 :param excludeModule: if True, module variables are always kept untouched
1860 :param simplify: try to simplify code (if we delete a declaration statement that used a
1861 variable as kind selector, and if this variable is not used else where,
1862 we also delete it)
1863 :return: the varList without the unremovable variables
1864 If possible, remove the variable from declaration, and from the argument list if needed
1865 """
1866 varList = self._normalizeUniqVar(varList)
1867 if excludeModule:
1868 varList = [v for v in varList if v[0].split('/')[-1].split(':')[0] != 'module']
1869
1870 varUsed = self.isVarUsed(varList, dummyAreAlwaysUsed=excludeDummy)
1871 varListToRemove = []
1872 for scopePath, varName in varList:
1873 assert scopePath.split('/')[-1].split(':')[0] != 'type', \
1874 "The removeVarIfUnused cannot be used with type members"
1875 if not varUsed[(scopePath, varName)]:
1876 varListToRemove.append([scopePath, varName])
1877 self.removeVar(varListToRemove, simplify=simplify)
1878 return varListToRemove
1879
1880 @debugDecor
1881 def isVarUsed(self, varList, exactScope=False, dummyAreAlwaysUsed=False):
1882 """
1883 :param varList: list of variables to test. Each item is a list or tuple of two elements.
1884 The first one describes where the variable is declared, the second one is
1885 the name of the variable. The first element is a '/'-separated path with
1886 each element having the form 'module:<name of the module>',
1887 'sub:<name of the subroutine>' or 'func:<name of the function>'
1888 :param exactScope: True to search strictly in scope
1889 :param dummyAreAlwaysUsed: Returns True if variable is a dummy argument
1890 :return: a dict whose keys are the elements of varList, and values are True when the
1891 variable is used, False otherwise
1892
1893 If exactScope is True, the function will search for variable usage
1894 only in this scope. But this feature has a limited interest.
1895
1896 If exactScope is False:
1897 - if scopePath is a subroutine/function in a contains section,
1898 and if the variable is not declared in this scope, usages are
1899 searched in the module/subroutine/function upper that declared
1900 the variable and in all subroutines/functions in the contains section
1901 - if scopePath is a module/subroutine/function that has a
1902 contains sections, usages are searched in all subroutines/functions
1903 in the contains section
1904
1905 To know if a variable can be removed, you must use exactScope=False
1906 """
1907 varList = self._normalizeUniqVar(varList)
1908 # We must not limit to self.getScopes because var can be used upper than self
1909 allScopes = {scope.path: scope for scope in self.mainScope.getScopes()}
1910
1911 # Computes in which scopes variable must be searched
1912 if exactScope:
1913 locsVar = {(scopePath, varName): [scopePath]
1914 for scopePath, varName in varList}
1915 else:
1916 locsVar = {}
1917 for scopePath, varName in varList:
1918 # We search everywhere if var declaration is not found
1919 # Otherwise, we search from the scope where the variable is declared
1920 var = allScopes[scopePath].varList.findVar(varName)
1921 path = scopePath.split('/')[0] if var is None else var['scopePath']
1922
1923 # We start search from here but we must include all routines in contains
1924 # that do not declare again the same variable name
1925 testScopes = [path] # we must search in the current scope
1926 for scPath, sc in allScopes.items():
1927 if scPath.startswith(path + '/') and \
1928 scPath.split('/')[-1].split(':')[0] != 'type':
1929 # sc is a scope path contained inside path and is not a type declaration
1930 if sc.varList.findVar(varName, exactScope=True) is None:
1931 # There is not another variable with same name declared inside
1932 testScopes.append(scPath) # if variable is used here, it is used
1933 locsVar[(scopePath, varName)] = testScopes
1934
1935 # For each scope to search, list all the variables used
1936 usedVar = {}
1937 for scopePath in list(set(item for sublist in locsVar.values() for item in sublist)):
1938 usedVar[scopePath] = []
1939 # Loop on all child in the scope
1940 for node in allScopes[scopePath]:
1941 # we don't want use statement, it could be where the variable is declared,
1942 # not a usage place
1943 if not tag(node) == 'use-stmt':
1944 if tag(node) == 'T-decl-stmt':
1945 # We don't want the part with the list of declared variables, we only want
1946 # to capture variables used in the kind selector or in the shape
1947 # specification
1948 nodesN = node.findall('.//{*}_T-spec_//{*}N') + \
1949 node.findall('.//{*}shape-spec//{*}N')
1950 else:
1951 nodesN = node.findall('.//{*}N')
1952
1953 # We look for the variable name in these 'N' nodes.
1954 for nodeN in nodesN:
1955 if dummyAreAlwaysUsed:
1956 # No need to if the variable is a dummy argument; because if it is
1957 # one it will be found in the argument list of the subroutine/function
1958 # and will be considered as used
1959 usedVar[scopePath].append(n2name(nodeN).upper())
1960 else:
1961 parPar = allScopes[scopePath].getParent(nodeN, 2) # parent of parent
1962 # We exclude dummy argument list to really check if the variable is used
1963 # and do not only appear as an argument of the subroutine/function
1964 if parPar is None or not tag(parPar) == 'dummy-arg-LT':
1965 usedVar[scopePath].append(n2name(nodeN).upper())
1966
1967 result = {}
1968 for scopePath, varName in varList:
1969 assert scopePath.split('/')[-1].split(':')[0] != 'type', \
1970 'We cannot check type component usage'
1971 result[(scopePath, varName)] = any(varName.upper() in usedVar[scopePath]
1972 for scopePath in locsVar[(scopePath, varName)])
1973
1974 return result
1975
1976 @debugDecor
1977 @updateVarList
1978 @updateTree('signal')
1979 def addONLY(self, parserOptions=None, wrapH=False):
1980 """
1981 Adds missing ONLY clause to USE statements
1982 :param parserOptions, wrapH: see the PYFT class
1983 """
1984 for useStmt in self.findall('.//{*}use-stmt'):
1985 module = useStmt.find('.//{*}module-N')
1986 if module.tail is None or module.tail.replace(' ', '').upper() != ',ONLY:':
1987 # A USE statement without the ONLY clause has been found
1988 modulename = n2name(module.find('.//{*}N')).upper()
1989 modulescope = 'module:' + modulename
1990 modulefile = self.tree.scopeToFiles(modulescope)
1991 if len(modulefile) != 1:
1992 raise PYFTError(f'No or several files define the {modulename} module')
1993 symbols = []
1994 pft = None
1995 try:
1996 # Open file to get the symbol list defined in the module
1997 if self.getFileName() == os.path.normpath(modulefile[0]):
1998 # interface declared in same file
1999 xml = self.mainScope
2000 pft = None
2001 else:
2003 modulefile[0], parserOptions, wrapH, tree=self.tree,
2004 clsPYFT=self._mainScope.__class__)
2005 xml = pft
2006
2007 # variable list
2008 symbols.extend(v['n'] for v in xml.varList.restrict(modulescope, True))
2009 # subroutine, functions and interfaces
2010 for scope in xml.getScopeNode(modulescope,
2011 excludeContains=False).getScopes(
2012 level=2,
2013 excludeContains=False,
2014 includeItself=False):
2015 if scope.path.split('/')[1].split(':')[0] == 'interface':
2016 if scope.path.split('/')[1].split(':')[1] != '--UNKNOWN--':
2017 # Named interface is a symbol
2018 symbols.append(scope.path.split('/')[1].split(':')[1])
2019 if len(scope.path.split('/')) == 3:
2020 # Subroutine and functions defined in the interface
2021 symbols.append(scope.path.split('/')[2].split(':')[1])
2022 else:
2023 symbols.append(scope.path.split('/')[1].split(':')[1])
2024 symbols = sorted(set(symbols))
2025 # Add all the symbols in the ONLY clause
2026 # <f:use-stmt>USE <f:module-N><f:N><f:n>MODNAME</f:n></f:N></f:module-N>, ONLY:
2027 # <f:rename-LT>
2028 # <f:rename><f:use-N><f:N><f:n>S1</f:n></f:N></f:use-N></f:rename>,
2029 # <f:rename><f:use-N><f:N><f:n>S2</f:n></f:N></f:use-N></f:rename>
2030 # </f:rename-LT></f:use-stmt>
2031 module.tail = ', ONLY: '
2032 renameLT = createElem('rename-LT')
2033 useStmt.append(renameLT)
2034 rename = None
2035 parent = self.getScopePath(useStmt)
2036 isVarUsed = self.isVarUsed([(parent, symbol.upper()) for symbol in symbols])
2037 for symbol in symbols:
2038 if isVarUsed[(parent, symbol.upper())]:
2039 useN = createElem('use-N', childs=createElem('N',
2040 childs=createElem('n', text=symbol)))
2041 rename = createElem('rename', tail=', ', childs=useN)
2042 renameLT.append(rename)
2043 if rename is None:
2044 # Module is unused, we suppress it
2045 previous = self.getSiblings(useStmt, before=True, after=False)
2046 previous = None if len(previous) == 0 else previous[-1]
2047 if previous is not None and useStmt.tail is not None:
2048 if previous.tail is None:
2049 previous.tail = ''
2050 previous.tail += useStmt.tail
2051 self.getParent(useStmt).remove(useStmt)
2052 self.tree.signal(self) # Tree must be updated
2053 else:
2054 # Remove the comma after the last child
2055 rename.tail = None
2056 finally:
2057 if pft is not None:
2058 pft.close()
2059
2060 @debugDecor
2061 @updateVarList
2062 def addArgInTree(self, varName, declStmt, pos, stopScopes, moduleVarList=None,
2063 otherNames=None,
2064 parserOptions=None, wrapH=False):
2065 """
2066 Adds an argument to the routine and propagates it upward until we encounter a scope
2067 where the variable exists or a scope in stopScopes
2068 :param varName: variable name
2069 :param declStmt: declarative statment (will be used by addVar)
2070 :param pos: position of the variable in the list of dummy argument
2071 :param stopScopes: list of scopes to reach
2072 :param moduleVarList: list of module variable specification to insert in the xml code
2073 a module variable specification is a list of two elements:
2074 - module name
2075 - variable name or or list of variable names
2076 or None to add a USE statement without the ONLY attribute
2077 use moduleVarList to not add module variables
2078 :param otherNames: None or list of other variable names that can be used
2079 These variables are used first
2080 :param parserOptions, wrapH: see the PYFT class
2081
2082 Argument is inserted only on paths leading to one of scopes listed in stopScopes
2083 """
2084 def insertInArgList(varName, varNameToUse, pos, callFuncStmt):
2085 """
2086 Insert varName in the list of arguments to the subroutine or function call
2087 :param varName: name of the dummy argument
2088 :param varNameToUse: name of the variable
2089 :param pos: inclusion position
2090 :param callFuncStmt: call statement or function call
2091 """
2092 argList = callFuncStmt.find('./{*}R-LT/{*}parens-R/{*}element-LT')
2093 if argList is not None:
2094 container = createElem('element')
2095 else:
2096 argList = callFuncStmt.find('./{*}arg-spec')
2097 container = createElem('arg')
2098 if argList is None:
2099 # Call without argument
2100 callFuncStmt.find('./{*}procedure-designator').tail = '('
2101 argList = createElem('arg-spec', tail=')')
2102 callFuncStmt.append(argList)
2103 item = createExprPart(varNameToUse)
2104 previous = pos - 1 if pos >= 0 else len(argList) + pos # convert negative pos
2105 while previous >= 0 and tag(argList[previous]) in ('C', 'cnt'):
2106 previous -= 1
2107 following = pos if pos > 0 else len(argList) + pos + 1 # convert negative pos
2108 while following <= len(argList) - 1 and tag(argList[following]) in ('C', 'cnt'):
2109 following += 1
2110 if (previous >= 0 and argList[previous].find('./{*}arg-N/{*}k') is not None) or \
2111 (following <= len(argList) - 1 and
2112 argList[following].find('./{*}arg-N/{*}k') is not None) or \
2113 following == len(argList):
2114 # We use the key=val syntax whereever it is possible because it's safer in case of
2115 # optional arguments
2116 # If previous arg, or following arg is already with a key=val syntax, we can (must)
2117 # use it
2118 # If the inserted argument is the last one of the list, it also can use this syntax
2119 k = createElem('k', text=varName)
2120 argN = createElem('arg-N', tail='=')
2121 argN.append(k)
2122 argN.set('n', varName)
2123 container.append(argN)
2124 container.append(item)
2125 self.insertInList(pos, container, argList)
2126
2127 if self.path in stopScopes or self.tree.isUnderStopScopes(self.path, stopScopes):
2128 # We are on the path to a scope in the stopScopes list, or scopeUp is one of the
2129 # stopScopes
2130 var = self.varList.findVar(varName, exactScope=True)
2131 if otherNames is not None:
2132 vOther = [self.varList.findVar(v, exactScope=True) for v in otherNames]
2133 vOther = [v for v in vOther if v is not None]
2134 if len(vOther) > 0:
2135 var = vOther[-1]
2136
2137 if var is None:
2138 # The variable doesn't exist in this scope, we add it
2139 self.addVar([[self.path, varName, declStmt, pos]])
2140 if moduleVarList is not None:
2141 # Module variables must be added when var is added
2142 self.addModuleVar([(self.path, moduleName, moduleVarNames)
2143 for (moduleName, moduleVarNames) in moduleVarList])
2144
2145 # We look for interface declaration if subroutine is directly accessible
2146 if len(self.path.split('/')) == 1:
2147 filename, scopePathInterface = self.tree.findScopeInterface(self.path)
2148 if filename is not None:
2149 pft = None
2150 try:
2151 if self.getFileName() == os.path.normpath(filename):
2152 # interface declared in same file
2153 xml = self.mainScope
2154 pft = None
2155 else:
2157 filename, parserOptions, wrapH, tree=self.tree,
2158 clsPYFT=self._mainScope.__class__)
2159 xml = pft
2160 scopeInterface = xml.getScopeNode(scopePathInterface)
2161 varInterface = scopeInterface.varList.findVar(varName, exactScope=True)
2162 if varInterface is None:
2163 scopeInterface.addVar([[scopePathInterface, varName,
2164 declStmt, pos]])
2165 if moduleVarList is not None:
2166 # Module variables must be added when var is added
2167 xml.addModuleVar(
2168 [(scopePathInterface, moduleName, moduleVarNames)
2169 for (moduleName, moduleVarNames) in moduleVarList])
2170 if pft is not None:
2171 pft.write()
2172 finally:
2173 if pft is not None:
2174 pft.close()
2175
2176 if var is None and self.path not in stopScopes:
2177 # We must propagates upward
2178 # scopes calling the current scope
2179 for scopePathUp in self.tree.calledByScope(self.path):
2180 if scopePathUp in stopScopes or self.tree.isUnderStopScopes(scopePathUp,
2181 stopScopes):
2182 # We are on the path to a scope in the stopScopes list, or scopePathUp is
2183 # one of the stopScopes
2184 # can be defined several times?
2185 for filename in self.tree.scopeToFiles(scopePathUp):
2186 pft = None
2187 try:
2188 if self.getFileName() == os.path.normpath(filename):
2189 # Upper scope is in the same file
2190 xml = self.mainScope
2191 pft = None
2192 else:
2194 filename, parserOptions, wrapH,
2195 tree=self.tree,
2196 clsPYFT=self._mainScope.__class__)
2197 xml = pft
2198 scopeUp = xml.getScopeNode(scopePathUp)
2199 # Add the argument and propagate upward
2200 scopeUp.addArgInTree(
2201 varName, declStmt, pos,
2202 stopScopes, moduleVarList, otherNames,
2203 parserOptions=parserOptions,
2204 wrapH=wrapH)
2205 # Add the argument to calls (subroutine or function)
2206 name = self.path.split('/')[-1].split(':')[1].upper()
2207 isCalled = False
2208 varNameToUse = varName
2209 if otherNames is not None:
2210 vOther = [scopeUp.varList.findVar(v, exactScope=True)
2211 for v in otherNames]
2212 vOther = [v for v in vOther if v is not None]
2213 if len(vOther) > 0:
2214 varNameToUse = vOther[-1]['n']
2215 if self.path.split('/')[-1].split(':')[0] == 'sub':
2216 # We look for call statements
2217 for callStmt in scopeUp.findall('.//{*}call-stmt'):
2218 callName = n2name(callStmt.find(
2219 './{*}procedure-designator/{*}named-E/{*}N')).upper()
2220 if callName == name:
2221 insertInArgList(varName, varNameToUse, pos, callStmt)
2222 isCalled = True
2223 else:
2224 # We look for function use
2225 for funcCall in scopeUp.findall(
2226 './/{*}named-E/{*}R-LT/{*}parens-R/' +
2227 '{*}element-LT/../../..'):
2228 funcName = n2name(funcCall.find('./{*}N')).upper()
2229 if funcName == name:
2230 insertInArgList(varName, varNameToUse, pos, funcCall)
2231 isCalled = True
2232 if pft is not None:
2233 pft.write()
2234 finally:
2235 if pft is not None:
2236 pft.close()
2237
2238 if isCalled:
2239 # We must check in the scope (or upper scopes) if an interface
2240 # block declares the routine
2241 for interface in self.findall('.//{*}interface-construct/{*}' +
2242 'program-unit/{*}subroutine-stmt/' +
2243 '{*}subroutine-N/{*}N/../../../'):
2244 if n2name(interface.find('./{*}subroutine-stmt/' +
2245 '{*}subroutine-N/' +
2246 '{*}N')).upper() == name:
2247 # We must add the argument to the interface
2248 raise PYFTError('This case is not yet implemented')
restrict(self, scopePath, excludeContains)
Definition variables.py:241
findVar(self, varName, array=None, exactScope=False, extraVarList=None)
Definition variables.py:275
__init__(self, mainScope, _preCompute=None)
Definition variables.py:115
renameVar(self, oldName, newName)
removeUnusedLocalVar(self, excludeList=None, simplify=False)
isVarUsed(self, varList, exactScope=False, dummyAreAlwaysUsed=False)
findArrayBounds(self, arr, loopVar, extraVarList=None)
_normalizeScopeVar(self, scopeVarList)
Definition variables.py:436
addArgInTree(self, varName, declStmt, pos, stopScopes, moduleVarList=None, otherNames=None, parserOptions=None, wrapH=False)
addONLY(self, parserOptions=None, wrapH=False)
arrayR2parensR(self, namedE, table)
checkKeyDimConsistency(self, mustRaise=False, stopScopes=None)
Definition variables.py:610
varSpec2stmt(varSpec, implicitDeclaration=False)
addModuleVar(self, moduleVarList)
Definition variables.py:949
checkUnusedLocalVar(self, mustRaise=False, excludeList=None)
addArrayParenthesesInNode(self, node)
removeArrayParenthesesInNode(self, node)
checkIntent(self, mustRaise=False)
Definition variables.py:544
modifyAutomaticArrays(self, declTemplate=None, startTemplate=None, endTemplate=None)
checkONLY(self, mustRaise=False)
Definition variables.py:576
addExplicitArrayBounds(self, node=None)
_normalizeUniqVar(self, scopeVarList)
Definition variables.py:445
removeVarIfUnused(self, varList, excludeDummy=False, excludeModule=False, simplify=False)
checkImplicitNone(self, mustRaise=False)
Definition variables.py:506
findIndexArrayBounds(self, arr, index, loopVar)
removeVar(self, varList, simplify=False)
Definition variables.py:678
conservativePYFT(filename, parserOptions, wrapH, tree=None, verbosity=None, clsPYFT=None)
Definition pyfortool.py:37
_getDeclStmtTag(scopePath)
Definition variables.py:46