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 for scope in self.getScopes():
526 # The IMPLICIT NONE statement is inherited from the top unit, control at top
527 # unit is enough apart for INTERFACE blocs
528 if (scope.path.count('/') == 0 or
529 (scope.path.count('/') >= 2 and
530 scope.path.split('/')[-2].startswith('interface:'))):
531 if scope.find('./{*}implicit-none-stmt') is None:
532 message = "The 'IMPLICIT NONE' statment is missing in file " + \
533 "'{file}' for {scopePath}.".format(file=scope.getFileName(),
534 scopePath=scope.path)
535 if mustRaise:
536 logging.error(message)
537 raise PYFTError(message)
538 logging.warning(message)
539
540 @debugDecor
541 def checkIntent(self, mustRaise=False):
542 """
543 Check for missing INTENT attributes on dummy arguments.
544
545 Issues a logging warning for each dummy argument without an INTENT attribute.
546 When mustRaise is True, logs an error and raises PYFTError.
547
548 Parameters
549 ----------
550 mustRaise : bool, optional
551 If False (default), issue warnings and continue.
552 If True, issue errors and raise PYFTError.
553
554 Examples
555 --------
556 >>> pft = PYFT('input.F90')
557 >>> pft.checkIntent() # Issues warning for missing INTENT
558 >>> pft.checkIntent(mustRaise=True) # Raises error
559 """
560 ok = True
561 log = logging.error if mustRaise else logging.warning
562 for var in self.varList:
563 if var['arg'] and var['i'] is None:
564 log(("The dummy argument {} has no INTENT attribute, in " +
565 "file '{}'").format(var['n'], self.getFileName()))
566 ok = False
567 if not ok and mustRaise:
568 raise PYFTError("There are dummy arguments without INTENT attribute in " +
569 "file '{}'".format(self.getFileName()))
570
571 @debugDecor
572 def checkONLY(self, mustRaise=False):
573 """
574 Check for missing ONLY clauses in USE statements.
575
576 Issues a logging warning for each USE statement not followed by an ONLY clause.
577 When mustRaise is True, logs an error and raises PYFTError.
578
579 Parameters
580 ----------
581 mustRaise : bool, optional
582 If False (default), issue warnings and continue.
583 If True, issue errors and raise PYFTError.
584
585 Examples
586 --------
587 >>> pft = PYFT('input.F90')
588 >>> pft.checkONLY() # Issues warning for missing ONLY
589 WARNING: USE MODULE is not followed by an ONLY clause...
590 >>> pft.checkONLY(mustRaise=True) # Raises error
591 """
592 ok = True
593 log = logging.error if mustRaise else logging.warning
594 for useStmt in self.findall('.//{*}use-stmt'):
595 module = useStmt.find('.//{*}module-N')
596 if module.tail is None or module.tail.replace(' ', '').upper() != ',ONLY:':
597 log(f"USE {n2name(module.find('.//{*}N'))} is not followed by an ONLY clause " +
598 f"in file'{self.getFileName()}'.")
599 if not ok and mustRaise:
600 raise PYFTError("There are USE statements not followed by an ONLY clause " +
601 f"file '{self.getFileName()}'")
602
603 @debugDecor
604 @noParallel
605 @updateVarList
606 @updateTree('signal')
607 def removeVar(self, varList, simplify=False):
608 """
609 Remove variables from declarations and argument lists.
610
611 Parameters
612 ----------
613 varList : list of tuple
614 List of variables to remove. Each item is a list or tuple of two elements:
615 - First element: scope path where the variable is used (or declared).
616 This is a '/' separated path where each element has the form:
617 'module:<name>', 'sub:<name>', 'func:<name>', or 'type:<name>'.
618 - Second element: variable name (string).
619
620 Example: [('module:MOD/sub:SUB', 'X'), ('module:MOD/sub:SUB', 'Y')]
621 simplify : bool, optional
622 If True, also remove variables that become unused after the deletion
623 (e.g., kind selectors used only by the removed variable).
624
625 Examples
626 --------
627 Remove variable X from subroutine SUB in module MOD:
628 >>> pft = PYFT('input.F90')
629 >>> pft.removeVar([('module:MOD/sub:SUB', 'X')])
630
631 Remove multiple variables with simplification:
632 >>> pft.removeVar([('module:MOD/sub:SUB', 'KIND_VAR')], simplify=True)
633
634 Notes
635 -----
636 - Dummy arguments are removed from both declarations and argument lists.
637 - USE statement variables are removed from ONLY clauses.
638 - If all variables in a declaration statement are removed, the statement
639 itself is deleted (unless simplify=True, which may delete additional unused variables).
640 """
641 varList = self._normalizeUniqVar(varList)
642
643 # Sort scopes by depth
644 sortedVarList = {}
645 for scopePath, varName in varList:
646 nb = scopePath.count('/')
647 sortedVarList[nb] = sortedVarList.get(nb, []) + [(scopePath, varName.upper())]
648
649 varToRemoveIfUnused = []
650 # Loop on varList starting by inner most variables
651 nbList = [] if len(sortedVarList.keys()) == 0 else \
652 range(max(sortedVarList.keys()) + 1)[::-1]
653 for nb in nbList:
654 sortedVarList[nb] = sortedVarList.get(nb, [])
655 # Loop on scopes
656 for scopePath in list(set(scopePath for scopePath, _ in sortedVarList[nb])):
657 # use of mainScope because variable can be declared upper than self
658 scope = self.mainScope.getScopeNode(scopePath)
659 # Variables searched in this scope
660 varNames = list(set(v for (w, v) in sortedVarList[nb] if w == scopePath))
661 declStmt = _getDeclStmtTag(scopePath)
662 # If scopePath is "module:XX/sub:YY", getScopeNode returns a node
663 # containing the subroutine declaration statements and
664 # excluding the subroutine and functions potentially included
665 # after a "contains" statement
666 previous = None
667 # list() to allow removing during the iteration
668 for node in list(scope):
669 deleted = False
670
671 # Checks if variable is a dummy argument
672 dummyList = node.find('{*}dummy-arg-LT') # This is the list of the dummies
673 if dummyList is not None:
674 # Loop over all dummy arguments
675 for arg in dummyList.findall('.//{*}arg-N'):
676 name = n2name(arg.find('.//{*}N')).upper()
677 for varName in [v for v in varNames if v == name]:
678 # This dummy arg is a searched variable, we remove it from the list
679 scope.removeFromList(arg, dummyList)
680
681 # In case the variable is declared
682 if tag(node) == declStmt:
683 # We are in a declaration statement
684 # list of declaration in the current statment
685 declList = node.find('./{*}EN-decl-LT')
686 for enDecl in declList.findall('.//{*}EN-decl'):
687 name = n2name(enDecl.find('.//{*}N')).upper()
688 for varName in [v for v in varNames if v == name]:
689 # The argument is declared here,
690 # we suppress it from the declaration list
691 varNames.remove(varName)
692 scope.removeFromList(enDecl, declList)
693 # In all the variables are suppressed from the declaration statement
694 if len(list(declList.findall('./{*}EN-decl'))) == 0:
695 if simplify:
696 varToRemoveIfUnused.extend([[scopePath, n2name(nodeN)]
697 for nodeN in node.findall('.//{*}N')])
698 # We will delete the current node but we don't want to lose
699 # any text. So, we put the node's text in the tail of the previous node
700 if previous is not None and node.tail is not None:
701 if previous.tail is None:
702 previous.tail = ''
703 previous.tail += node.tail
704 deleted = True
705 scope.getParent(node).remove(node)
706
707 # In case the variable is a module variable
708 if tag(node) == 'use-stmt':
709 # We are in a use statement
710 useList = node.find('./{*}rename-LT')
711 if useList is not None:
712 for rename in useList.findall('.//{*}rename'):
713 name = n2name(rename.find('.//{*}N')).upper()
714 for varName in [v for v in varNames if v == name]:
715 varNames.remove(varName)
716 # The variable is declared here, we remove it from the list
717 scope.removeFromList(rename, useList)
718 # In case the variable was alone
719 attribute = node.find('{*}module-N').tail
720 if attribute is None:
721 attribute = ''
722 attribute = attribute.replace(' ', '').replace('\n', '')
723 attribute = attribute.replace('&', '').upper()
724 useList = node.find('./{*}rename-LT')
725 if len(useList) == 0 and attribute[0] == ',' and \
726 attribute[1:] == 'ONLY:':
727 # If there is a 'ONLY' attribute,
728 # we suppress the use statement entirely
729 if previous is not None and node.tail is not None:
730 if previous.tail is None:
731 previous.tail = ''
732 previous.tail += node.tail
733 deleted = True
734 scope.getParent(node).remove(node)
735 scope.tree.signal(scope) # Tree must be updated
736 elif len(useList) == 0:
737 # there is no 'ONLY' attribute
738 moduleName = scope.getSiblings(useList, before=True,
739 after=False)[-1]
740 previousTail = moduleName.tail
741 if previousTail is not None:
742 moduleName.tail = previousTail.replace(',', '')
743 scope.getParent(useList).remove(useList)
744 # end loop if all variables have been found
745 if len(varNames) == 0:
746 break
747 # Store node for the following iteration
748 if not deleted:
749 previous = node
750
751 # If some variables have not been found, they are certainly declared one level upper
752 if len(varNames) != 0:
753 newWhere = '/'.join(scopePath.split('/')[:-1])
754 sortedVarList[nb - 1] = sortedVarList.get(nb - 1, []) + \
755 [(newWhere, varName) for varName in varNames]
756
757 if simplify and len(varToRemoveIfUnused) > 0:
758 self.removeVarIfUnused(varToRemoveIfUnused, excludeDummy=True, simplify=True)
759
760 @debugDecor
761 @updateVarList
762 def addVar(self, varList):
763 """
764 Add variables to declarations and argument lists.
765
766 Parameters
767 ----------
768 varList : list of list/tuple
769 List of variable specifications to insert. Each specification is a list
770 of four elements:
771 - Scope path (str): path to the module, subroutine, function, or type
772 where the variable should be declared (e.g., 'module:MOD/sub:SUB').
773 - Variable name (str): name of the variable to add.
774 - Declaration statement (str): FORTRAN declaration (e.g., 'REAL, INTENT(IN) :: X').
775 - Position (int or None): position in dummy argument list for arguments,
776 None for local variables.
777
778 Examples
779 --------
780 Add a local variable:
781 >>> pft = PYFT('input.F90')
782 >>> pft.addVar([('module:MOD/sub:SUB', 'LOCAL_VAR', 'INTEGER :: LOCAL_VAR', None)])
783
784 Add a dummy argument at position 0:
785 >>> pft.addVar([('module:MOD/sub:SUB', 'ARG', 'REAL, INTENT(IN) :: ARG', 0)])
786
787 Add multiple variables:
788 >>> pft.addVar([
789 ... ('module:MOD/sub:SUB', 'X', 'REAL :: X', None),
790 ... ('module:MOD/sub:SUB', 'Y', 'INTEGER :: Y', None)
791 ... ])
792
793 Notes
794 -----
795 - If adding to an argument list, the declaration is automatically updated
796 with the INTENT attribute if specified.
797 - Declaration statements are inserted before the first executable statement.
798 """
799 varList = self._normalizeUniqVar(varList)
800
801 for (scopePath, name, declStmt, pos) in varList:
802 scope = self.getScopeNode(scopePath)
803
804 # Add variable to the argument list
805 if pos is not None:
806 argN = createElem('arg-N')
807 nodeN = createElem('N')
808 nodeN.append(createElem('n', text=name))
809 argN.append(nodeN)
810 # search for a potential node, within the scope, with a list of dummy arguments
811 argLst = scope.find('.//{*}dummy-arg-LT')
812 if argLst is None:
813 # This was a subroutine or function without dummy arguments
814 scope[0][0].tail = '('
815 argLst = createElem('dummy-arg-LT', tail=')')
816 scope[0].insert(1, argLst)
817 scope.insertInList(pos, argN, argLst)
818
819 # Declare the variable
820 # The following test is needed in case several variables are added in the argument list
821 # but the declaration statement is given only once for all the variables
822 if declStmt is not None and declStmt != '':
823 # Declaration statement tag according to path (member of type declaration or not)
824 declStmtTag = _getDeclStmtTag(scopePath)
825
826 if scopePath.split('/')[-1].split(':')[0] == 'type':
827 # Add declaration statement in type declaration
828 # Statement building
829 ds = createExpr(declStmt)[0]
830 previousTail = '\n' + declStmt[:re.search(r'\S', declStmt).start()]
831
832 # node insertion
833 # scope[0] is the T-stmt node, scope[-1] is the end-T-stmt node
834 # scope[-2] is the last node before the end-T-stmt node (last component,
835 # comment or the T-stmt node)
836 ds.tail = scope[-2].tail
837 scope[-2].tail = previousTail
838 scope.insert(-1, ds) # insert before last one
839
840 else:
841 # Add declaration statement (not type declaration case)
842 # Statement building
843 ds = createExpr(declStmt)[0]
844 previousTail = '\n' + declStmt[:re.search(r'\S', declStmt).start()]
845
846 # node insertion index
847 declLst = [node for node in scope if tag(node) == declStmtTag]
848 if len(declLst) != 0:
849 # There already are declaration statements
850 # We look for the last position in the declaration list which do not use
851 # the variable we add
852 for decl in declLst:
853 index = list(scope).index(decl)
854 if name in [n2name(nodeN) for nodeN in decl.findall('.//{*}N')]:
855 break
856 else:
857 # There is no declaration statement
858 # list of executable nodes
859 stmtLst = [node for node in scope if isExecutable(node)]
860 if len(stmtLst) == 0:
861 # There is no executable statement, we insert the declaration at the end
862 # Last node is the ending node (e.g. end-subroutine-stmt)
863 index = len(scope) - 1
864 else:
865 # We insert the declaration just before the first executable statement
866 index = list(scope).index(stmtLst[0])
867
868 # node insertion
869 if index != 0:
870 ds.tail = scope[index - 1].tail
871 scope[index - 1].tail = previousTail
872 scope.insert(index, ds)
873
874 @debugDecor
875 @noParallel
876 @updateVarList
877 @updateTree('signal')
878 def addModuleVar(self, moduleVarList):
879 """
880 Add USE statements for module variables.
881
882 Parameters
883 ----------
884 moduleVarList : list of list/tuple
885 List of module variable specifications. Each specification is a list
886 of three elements:
887 - Scope path (str): path to the location where USE should be added
888 (e.g., 'module:MOD/sub:SUB').
889 - Module name (str): name of the module to USE.
890 - Variable name(s) (str, list, or None):
891 - str: single variable name to import.
892 - list: list of variable names to import.
893 - None: add USE without ONLY clause (import all).
894
895 Examples
896 --------
897 Import single variable Y from MODD_XX into subroutine FOO:
898 >>> pft = PYFT('input.F90')
899 >>> pft.addModuleVar([('sub:FOO', 'MODD_XX', 'Y')])
900 ! Adds: USE MODD_XX, ONLY: Y
901
902 Import multiple variables:
903 >>> pft.addModuleVar([('sub:FOO', 'MODD_XX', ['X', 'Y', 'Z'])])
904 ! Adds: USE MODD_XX, ONLY: X, Y, Z
905
906 Add USE without ONLY (import all):
907 >>> pft.addModuleVar([('sub:FOO', 'MODD_XX', None)])
908 ! Adds: USE MODD_XX
909
910 Import into a module:
911 >>> pft.addModuleVar([('module:MOD', 'OTHER_MOD', 'VAR')])
912
913 Notes
914 -----
915 - Existing USE statements for the same module are updated to include new variables.
916 - Duplicate imports are avoided (variables already imported are not re-added).
917 """
918 moduleVarList = self._normalizeScopeVar(moduleVarList)
919
920 for (scopePath, moduleName, varName) in moduleVarList:
921 if varName is None:
922 varName = []
923 elif not isinstance(varName, list):
924 varName = [varName]
925 scope = self.getScopeNode(scopePath)
926
927 # USE statement already present
928 useLst = [node for node in scope if tag(node) == 'use-stmt']
929
930 # Check if we need to add a USE
931 insertUse = True
932 for us in useLst:
933 usName = n2name(us.find('.//{*}module-N//{*}N'))
934 usVar = [n2name(v.find('.//{*}N')).upper() for v in us.findall('.//{*}use-N')]
935 if len(varName) == 0 and len(usVar) == 0 and usName.upper() == moduleName.upper():
936 # There aleardy is a 'USE MODULE' and we wanted to insert a 'USE MODULE'
937 insertUse = False
938 elif len(varName) > 0 and len(usVar) > 0 and usName.upper() == moduleName.upper():
939 # There already is a 'USE MODULE, ONLY:' and we want to insert another
940 # 'USE MODULE, ONLY:'
941 # We suppress from the varName list, all the variables already defined
942 varName = [var for var in varName if var.upper() not in usVar]
943 if len(varName) == 0:
944 # All the variables we wanted to import are already defined
945 insertUse = False
946
947 if insertUse:
948 # Statement building
949 stmt = f'USE {moduleName}'
950 if len(varName) > 0:
951 stmt += ', ONLY:{}'.format(', '.join(varName))
952 us = createExpr(stmt)[0]
953
954 # node insertion index
955 if len(useLst) != 0:
956 # There already have use statements, we add the new one after them
957 index = list(scope).index(useLst[-1]) + 1
958 else:
959 # There is no use statement, we add the new node just after the first node
960 index = 1
961
962 us.tail = scope[index - 1].tail
963 scope[index - 1].tail = '\n'
964 scope.insert(index, us)
965 scope.tree.signal(scope) # Tree must be updated
966
967 @debugDecor
968 def showUnusedVar(self):
969 """
970 Display unused variables on stdout.
971
972 Searches through all scopes and displays variables that are declared
973 but never used in the code.
974
975 Examples
976 --------
977 >>> pft = PYFT('input.F90')
978 >>> pft.showUnusedVar()
979 Some variables declared in /module:MOD/sub:SUB are unused:
980 - LOCAL_VAR
981 - UNUSED_ARRAY
982 """
983 scopes = self.getScopes(excludeKinds=['type'])
984 varUsed = self.isVarUsed([(scope.path, v['n'])
985 for scope in scopes
986 for v in self.varList
987 if v['scopePath'] == scope.path])
988 for scope in scopes:
989 varList = [k[1].upper() for (k, v) in varUsed.items() if (not v) and k[0] == scope.path]
990 if len(varList) != 0:
991 print(f'Some variables declared in {scope.path} are unused:')
992 print(' - ' + ('\n - '.join(varList)))
993
994 @debugDecor
995 def checkUnusedLocalVar(self, mustRaise=False, excludeList=None):
996 """
997 Check for unused local variables in scopes.
998
999 Issues a logging warning for each local variable that is declared but never used.
1000 When mustRaise is True, logs an error and raises PYFTError.
1001
1002 Parameters
1003 ----------
1004 mustRaise : bool, optional
1005 If False (default), issue warnings and continue.
1006 If True, issue errors and raise PYFTError.
1007 excludeList : list of str, optional
1008 List of variable names to exclude from the check.
1009 These variables will not trigger warnings even if unused.
1010
1011 Examples
1012 --------
1013 >>> pft = PYFT('input.F90')
1014 >>> pft.checkUnusedLocalVar() # Issues warnings
1015 WARNING: The LOCAL_VAR variable is not used...
1016 >>> pft.checkUnusedLocalVar(excludeList=['TEMP']) # Exclude TEMP
1017 >>> pft.checkUnusedLocalVar(mustRaise=True) # Raises error
1018 """
1019
1020 if excludeList is None:
1021 excludeList = []
1022 else:
1023 excludeList = [v.upper() for v in excludeList]
1024 scopes = self.getScopes(excludeKinds=['type'])
1025 # We do not check dummy args, module variables
1026 varUsed = self.isVarUsed([(scope.path, v['n'])
1027 for scope in scopes
1028 for v in self.varList
1029 if (v['n'].upper() not in excludeList and
1030 (not v['arg']) and
1031 v['scopePath'].split('/')[-1].split(':')[0] != 'module' and
1032 v['scopePath'] == scope.path)])
1033 for scope in scopes:
1034 for var in [k[1].upper() for (k, v) in varUsed.items()
1035 if (not v) and k[0] == scope.path]:
1036 message = f"The {var} variable is not used in file " + \
1037 f"'{scope.getFileName()}' for {scope.path}."
1038 if mustRaise:
1039 logging.error(message)
1040 raise PYFTError(message)
1041 logging.warning(message)
1042
1043 @debugDecor
1044 def removeUnusedLocalVar(self, excludeList=None, simplify=False):
1045 """
1046 Remove unused local variables from declarations.
1047
1048 Removes variables that are declared but never used in the code.
1049 Dummy arguments and module variables are not removed.
1050
1051 Parameters
1052 ----------
1053 excludeList : list of str, optional
1054 List of variable names to exclude from removal.
1055 These variables will be kept even if unused.
1056 simplify : bool, optional
1057 If True, also remove variables that become unused after removal
1058 (e.g., kind selectors).
1059
1060 Examples
1061 --------
1062 >>> pft = PYFT('input.F90')
1063 >>> pft.removeUnusedLocalVar() # Remove all unused locals
1064
1065 Remove unused locals except TEMP and COUNTER:
1066 >>> pft.removeUnusedLocalVar(excludeList=['TEMP', 'COUNTER'])
1067
1068 With simplification (remove cascading unused vars):
1069 >>> pft.removeUnusedLocalVar(simplify=True)
1070
1071 Notes
1072 -----
1073 - Only local variables (declared in SUBROUTINE/FUNCTION scope) are removed.
1074 - Dummy arguments (subroutine parameters) are preserved.
1075 - Variables imported via USE statements are preserved.
1076 """
1077 if excludeList is None:
1078 excludeList = []
1079 else:
1080 excludeList = [item.upper() for item in excludeList]
1081
1082 allVar = [(scope.path, v['n'])
1083 for scope in self.getScopes(excludeKinds=['type'])
1084 for v in scope.varList
1085 if v['n'].upper() not in excludeList and v['scopePath'] == scope.path]
1086 self.removeVarIfUnused(allVar, excludeDummy=True, excludeModule=True, simplify=simplify)
1087
1088 @debugDecor
1089 def addExplicitArrayBounds(self, node=None):
1090 """
1091 Replace implicit array bounds with explicit bounds from declarations.
1092
1093 Transforms array slice notation (e.g., A(:)) into explicit bounds
1094 based on the array's declaration.
1095
1096 Parameters
1097 ----------
1098 node : xml element, optional
1099 Specific XML node to transform. If None (default), transforms
1100 all implicit bounds in all scopes.
1101
1102 Examples
1103 --------
1104 Given declaration: REAL, DIMENSION(1:10) :: A, B
1105 And usage: A(:) = B(:)
1106
1107 After transformation:
1108 A(1:10) = B(1:10)
1109
1110 Notes
1111 -----
1112 - Only handles 1D array slices (A(:) notation).
1113 - Does not modify allocatable arrays (where ':' is part of declaration).
1114 - Does not modify character type arrays.
1115 """
1116 if node is None:
1117 nodes = [(scope, scope) for scope in self.getScopes()]
1118 else:
1119 nodes = [(self, node)]
1120
1121 for (scope, childNode) in nodes:
1122 for parent4 in [parent4 for parent4
1123 in childNode.findall('.//{*}section-subscript/../../../..')
1124 if parent4.find('./{*}R-LT/{*}component-R') is None]:
1125 # Shape of type members is unknown
1126 for parent in parent4.findall('.//{*}section-subscript/..'):
1127 for sub in parent.findall('.//{*}section-subscript'):
1128 lowerUsed = sub.find('./{*}lower-bound')
1129 upperUsed = sub.find('./{*}upper-bound')
1130 # A slice can be A(:), A(I:) or A(:I), but not A(I)
1131 # With A(:) and A(:I), lowerUsed is None
1132 # With A(I:) lowerUsed.tail contains a ':'
1133 # With A(I) lowerUsed.tail doesn't contain a ':'
1134 if lowerUsed is None or \
1135 (lowerUsed.tail is not None and ':' in lowerUsed.tail):
1136 if lowerUsed is None or upperUsed is None:
1137 # At least one array bound is implicit
1138 varDesc = scope.varList.findVar(n2name(parent4.find('.//{*}N')))
1139 if varDesc is not None and varDesc['t'] is not None and \
1140 'CHAR' not in varDesc['t']: # module array or character
1141 lowerDecl, upperDecl = varDesc['as'][list(parent).index(sub)]
1142 if lowerDecl is None:
1143 lowerDecl = '1'
1144 # When a bound is explicit, we keep it, otherwise we
1145 # take the declared bound
1146 lowerBound = lowerDecl if lowerUsed is None \
1147 else alltext(lowerUsed)
1148 upperBound = upperDecl if upperUsed is None \
1149 else alltext(upperUsed)
1150 if upperBound is not None: # case of implicit shape
1151 lowerXml, upperXml = createArrayBounds(lowerBound,
1152 upperBound,
1153 'ARRAY')
1154 # We remove current explicit bounds or the ':',
1155 # and replace them by the new explicit declaration
1156 for nnn in sub:
1157 if tag(nnn) in ('lower-bound', 'upper-bound'):
1158 sub.remove(nnn)
1159 else:
1160 raise PYFTError("Unexpected case, " +
1161 "tag is {}".format(tag(nnn)))
1162 sub.text = '' # Delete the initial ':'
1163 sub.extend([lowerXml, upperXml])
1164
1165 @debugDecor
1166 @noParallel
1168 """
1169 Add explicit array parentheses to array variables.
1170
1171 Transforms array variable references to include explicit slice notation.
1172 For example, A becomes A(:) when A is declared as an array.
1173
1174 Examples
1175 --------
1176 Given declaration: REAL, DIMENSION(10) :: A, B, C
1177 And usage: A = B + C
1178
1179 After transformation:
1180 A(:) = B(:) + C(:)
1181
1182 Notes
1183 -----
1184 - Only modifies known arrays (declared with dimensions).
1185 - Excludes arrays in ALLOCATED, ASSOCIATED, and PRESENT intrinsic calls.
1186 - Does not modify pointer/allocatable arrays in call statements
1187 (to maintain interface compatibility).
1188 """
1189 # Loop on scopes
1190 for scope in self.getScopes():
1191 for node in scope.iter():
1192 # Arrays are used in statements
1193 # * We must exclude allocate-stmt, deallocate-stmt, pointer-a-stmt, T-decl-stmt and
1194 # associate-stmt,
1195 # nullify-stmt function-stmt, subroutine-stmt, interface-stmt must be kept
1196 # untouched
1197 # action-stmt is discarded as it contains another statement.
1198 # * Array variables to modify can be found in
1199 # - simple affectation (a-stmt),
1200 # - if construct conditions (if-then-stmt, else-if-stmt),
1201 # - where-construct mask (where-construct-stmt, else-where-stmt),
1202 # - if statement condition (if-stmt/condition-E, be carefull to not take the
1203 # whole if-stmt as it contains the action-stmt which,
1204 # in turn, can contain an allocate/deallocate/pointer
1205 # assignment)
1206 # - where statement mask (where-stmt/mask-E and not directly
1207 # where-stmt as for if-stmt),
1208 # - select-case-stmt and case-stmt,
1209 # - do-stmt or forall-construct-stmt (e.g. FOR I=LBOUND(A, 0), UBOUND(A, 0))
1210 # - forall-stmt/forall-triplet-spec-LT
1211 nodeToTransform = None
1212 if tag(node) in ('allocate-stmt', 'deallocate-stmt', 'pointer-a-stmt',
1213 'T-decl-stmt', 'associate-stmt', 'function-stmt',
1214 'subroutine-stmt', 'interface-stmt', 'action-stmt',
1215 'nullify-stmt'):
1216 # excluded
1217 pass
1218 elif tag(node) in ('if-stmt', 'where-stmt', 'forall-stmt'):
1219 # We must transform only a part of the node
1220 part = {'if-stmt': 'condition-E', 'where-stmt': 'mask-E',
1221 'forall-stmt': 'forall-triplet-spec-LT'}[tag(node)]
1222 nodeToTransform = node.find('./{*}' + part)
1223 elif tag(node).endswith('-stmt'):
1224 nodeToTransform = node
1225 if nodeToTransform is not None:
1226 scope.addArrayParenthesesInNode(nodeToTransform)
1227
1228 @debugDecor
1230 """
1231 Add explicit array parentheses to arrays within a specific XML node.
1232
1233 Parameters
1234 ----------
1235 node : xml element
1236 XML node in which to add array parentheses.
1237 Only processes named-E elements within this node.
1238
1239 See Also
1240 --------
1241 addArrayParentheses : Add parentheses in all scopes.
1242
1243 Examples
1244 --------
1245 >>> pft = PYFT('input.F90')
1246 >>> node = pft.find('.//{*}a-stmt')
1247 >>> pft.addArrayParenthesesInNode(node)
1248 """
1249 # Loop on variables
1250 for namedE in node.findall('.//{*}named-E'):
1251 if not namedE.find('./{*}R-LT'): # no parentheses
1252 if not self.isNodeInProcedure(namedE, ('ALLOCATED', 'ASSOCIATED', 'PRESENT')):
1253 # Pointer/allocatable used in ALLOCATED/ASSOCIATED must not be modified
1254 # Array in present must not be modified
1255 nodeN = namedE.find('./{*}N')
1256 var = self.varList.findVar(n2name(nodeN))
1257 if var is not None and var['as'] is not None and len(var['as']) > 0 and \
1258 not ((var['pointer'] or var['allocatable']) and self.isNodeInCall(namedE)):
1259 # This is a known array variable, with no parentheses
1260 # But we exclude pointer allocatable in call statement because the called
1261 # subroutine can wait for a pointer/allocatable and not an array (and it
1262 # is difficult to guess as it would need to find the source code of the
1263 # subroutine)
1264 nodeRLT = createElem('R-LT')
1265 namedE.insert(list(namedE).index(nodeN) + 1, nodeRLT)
1266 arrayR = createElem('array-R', text='(')
1267 nodeRLT.append(arrayR)
1268 sectionSubscriptLT = createElem('section-subscript-LT', tail=')')
1269 arrayR.append(sectionSubscriptLT)
1270 for _ in var['as']:
1271 sectionSubscript = createElem('section-subscript', text=':', tail=', ')
1272 sectionSubscriptLT.append(sectionSubscript)
1273 sectionSubscript.tail = None # last one
1274
1275 @debugDecor
1277 """
1278 Remove array parentheses if no index selection is needed.
1279
1280 When an array has full slice notation (e.g., A(:,:)) that matches
1281 the entire array, removes the parentheses.
1282
1283 Parameters
1284 ----------
1285 node : xml element
1286 XML node in which to remove unnecessary parentheses.
1287
1288 Transformation
1289 -------------
1290 Before: A(:,:) (when A is declared as 2D with full bounds)
1291 After: A
1292
1293 Notes
1294 -----
1295 - Only removes parentheses when all dimensions use full slice (:) notation.
1296 - Preserves parentheses if any dimension has explicit index selection.
1297 - Excludes arrays in ALLOCATED, ASSOCIATED, and PRESENT intrinsic calls.
1298 """
1299 # Loop on variables
1300 for namedE in node.findall('.//{*}named-E'):
1301 if namedE.find('./{*}R-LT'): # parentheses
1302 if not self.isNodeInProcedure(namedE, ('ALLOCATED', 'ASSOCIATED', 'PRESENT')):
1303 # Pointer/allocatable used in ALLOCATED/ASSOCIATED must not be modified
1304 # Array in present must not be modified
1305 nodeN = namedE.find('./{*}N')
1306 var = self.varList.findVar(n2name(nodeN))
1307 if var is not None and var['as'] is not None and len(var['as']) > 0 and \
1308 not ((var['pointer'] or var['allocatable']) and self.isNodeInCall(namedE)):
1309 arrayR = namedE.findall('./{*}R-LT')
1310 for arr in arrayR:
1311 sectionSubscriptLT = arr.findall('.//{*}section-subscript-LT')
1312 for ss in sectionSubscriptLT:
1313 lowerBound = ss.findall('.//{*}lower-bound')
1314 if len(lowerBound) == 0:
1315 # Node to be removed <f:R-LT><f:array-R><f:section-subscript-LT>
1316 par = self.getParent(ss, level=2)
1317 parOfpar = self.getParent(par)
1318 parOfpar.remove(par)
1319
1320 @debugDecor
1321 @updateVarList
1322 def modifyAutomaticArrays(self, declTemplate=None, startTemplate=None, endTemplate=None):
1323 """
1324 Transform automatic arrays using customizable templates.
1325
1326 Modifies automatic array declarations in subroutines and functions by
1327 applying templates for declaration, initialization, and cleanup.
1328
1329 Parameters
1330 ----------
1331 declTemplate : str, optional
1332 Template for the array declaration. If None, declaration is unchanged.
1333 startTemplate : str, optional
1334 Template for code to insert as the first executable statement
1335 (e.g., allocation).
1336 endTemplate : str, optional
1337 Template for code to insert as the last executable statement
1338 (e.g., deallocation).
1339
1340 Returns
1341 -------
1342 int
1343 Number of arrays modified.
1344
1345 Placeholders
1346 ------------
1347 Each template can use the following placeholders (case-sensitive):
1348
1349 ============ ==================================================
1350 Placeholder Description (for declaration "A(I, I:J, 0:I)")
1351 ============ ==================================================
1352 {name} Variable name (e.g., "A")
1353 {type} Type specification (e.g., "REAL")
1354 {doubledotshape} Colon-separated dimensions (e.g., ":, :, :")
1355 {shape} Bounds with indices (e.g., "I, I:J, 0:I")
1356 {lowUpList} Flattened bounds (e.g., "1, I, I, J, 0, I")
1357 ============ ==================================================
1358
1359 Examples
1360 --------
1361 Transform automatic arrays to allocatables:
1362
1363 >>> pft = PYFT('input.F90')
1364 >>> pft.modifyAutomaticArrays(
1365 ... declTemplate="{type}, DIMENSION({doubledotshape}), ALLOCATABLE :: {name}",
1366 ... startTemplate="ALLOCATE({name}({shape}))",
1367 ... endTemplate="DEALLOCATE({name})"
1368 ... )
1369
1370 Given input:
1371 REAL, DIMENSION(I, I:J, 0:I) :: A
1372 A = 1.0
1373
1374 Produces:
1375 REAL, DIMENSION(:,:,:), ALLOCATABLE :: A
1376 ALLOCATE(A(I, I:J, 0:I))
1377 A = 1.0
1378 DEALLOCATE(A)
1379
1380 Notes
1381 -----
1382 - Only processes automatic arrays (stack-allocated based on parameters).
1383 - Excludes dummy arguments, allocatable, pointer, and function result arrays.
1384 - Arrays with initial values are not processed.
1385 - Variables are processed in dependency order to handle interdependencies.
1386 """
1387 templates = {'decl': declTemplate if declTemplate is not None else '',
1388 'start': startTemplate if startTemplate is not None else '',
1389 'end': endTemplate if endTemplate is not None else ''} # ordered dict
1390
1391 number = 0
1392 for scope in [scope for scope in self.getScopes()
1393 if scope.path.split('/')[-1].split(':')[0] in ('sub', 'func')]:
1394 # For all subroutine and function scopes
1395 # Determine the list of variables to transform
1396 varListToTransform = []
1397 for var in [var for var in scope.varList
1398 if var['as'] is not None and
1399 len(var['as']) > 0 and
1400 'CHARACTER' not in var['t'] and
1401 not (var['arg'] or var['allocatable'] or
1402 var['pointer'] or var['result'])]:
1403 # For all automatic arrays, which are not argument, not allocatable,
1404 # not pointer and not result
1405 if var['init'] is None:
1406 # Array with initial value can't be processed by modifyAutomaticArrays
1407 varListToTransform.append(var)
1408 # A variable can make use of the size of another variable in its declaration statement
1409 # We order the variables to not insert the declaration of a variable before the
1410 # declaration of the variables it depends on
1411 orderedVarListToTransform = []
1412 while len(varListToTransform) > 0:
1413 nAdded = 0
1414 for var in varListToTransform[:]:
1415 # flatten var['asx'] excluding None
1416 listN = [x for dim in var['asx'] for x in dim if x is not None]
1417 listN = [n2name(nodeN).upper() for asx in listN
1418 for nodeN in asx.findall('.//{*}N/{*}n/..')]
1419 if len(set(listN).intersection([v['n'].upper()
1420 for v in varListToTransform])) == 0:
1421 # Variable var does not use variables still in varListToTransform
1422 varListToTransform.remove(var)
1423 orderedVarListToTransform.append(var)
1424 nAdded += 1
1425 if nAdded == 0:
1426 raise PYFTError('It seems that there is a circular reference in ' +
1427 'the declaration statements')
1428 # Loop on variable to transform
1429 for var in orderedVarListToTransform[::-1]: # reverse order
1430 number += 1
1431 # Apply the template
1432 templ = copy.deepcopy(templates)
1433 for templPart in templ:
1434 if '{doubledotshape}' in templ[templPart]:
1435 templ[templPart] = templ[templPart].replace(
1436 '{doubledotshape}', ','.join([':'] * len(var['as'])))
1437 if '{shape}' in templ[templPart]:
1438 result = []
1439 for i in range(len(var['as'])):
1440 if var['as'][i][0] is None:
1441 result.append(var['as'][i][1])
1442 else:
1443 result.append(var['as'][i][0] + ':' + var['as'][i][1])
1444 templ[templPart] = templ[templPart].replace('{shape}', ', '.join(result))
1445 if '{name}' in templ[templPart]:
1446 templ[templPart] = templ[templPart].replace('{name}', var['n'])
1447 if '{type}' in templ[templPart]:
1448 templ[templPart] = templ[templPart].replace('{type}', var['t'])
1449 if '{lowUpList}' in templ[templPart]:
1450 result = []
1451 for i in range(len(var['as'])):
1452 if var['as'][i][0] is None:
1453 result.extend([1, var['as'][i][1]])
1454 else:
1455 result.extend([var['as'][i][0], var['as'][i][1]])
1456 templ[templPart] = templ[templPart].replace(
1457 '{lowUpList}', ', '.join([str(r) for r in result]))
1458
1459 # Get the xml for the template
1460 separator = "!ABCDEFGHIJKLMNOPQRSTUVWabcdefghijklmnopqrstuvwxyz0123456789"
1461 part = 0
1462 for node in createExpr(templ['decl'] + '\n' + separator + '\n' +
1463 templ['start'] + '\n' + separator + '\n' +
1464 templ['end']):
1465 templPart = list(templ.keys())[part]
1466 if not isinstance(templ[templPart], list):
1467 templ[templPart] = []
1468 if tag(node) == 'C' and node.text == separator:
1469 part += 1
1470 else:
1471 templ[templPart].append(node)
1472
1473 # Replace declaration statement
1474 # We look for the last position in the declaration list which do not use the
1475 # variable we add
1476 # This algorithm will stop when the original declaration statement is encoutered
1477 for decl in scope.findall('./{*}T-decl-stmt'):
1478 index = list(scope).index(decl)
1479 if var['n'] in [n2name(nodeN) for nodeN in decl.findall('.//{*}N')]:
1480 break
1481 scope.removeVar([(scope.path, var['n'])], simplify=False)
1482 for nnn in templ['decl'][::-1]:
1483 scope.insert(index, nnn)
1484
1485 # Insert statements
1486 for nnn in templ['start'][::-1]:
1487 scope.insertStatement(nnn, True)
1488 for nnn in templ['end'][::-1]:
1489 scope.insertStatement(nnn, False)
1490 return number
1491
1492 @staticmethod
1493 @debugDecor
1494 def varSpec2stmt(varSpec, implicitDeclaration=False):
1495 """
1496 :param varSpec: a variable description, same form as the items return by self.varList
1497 :param implicitDeclaration: True if the variable may contain implicit declaration
1498 (e.g. outside of PHYEX)
1499 :return: the associated declarative statement
1500 """
1501 if varSpec['use'] is not False:
1502 stmt = f"USE {varSpec['use']}, ONLY: {varSpec['n']}"
1503 else:
1504 stmt = varSpec['t']
1505 if varSpec['as']:
1506 stmt += ', DIMENSION('
1507 dl = []
1508 for el in varSpec['as']:
1509 if el[0] is None and el[1] is None:
1510 dl.append(':')
1511 elif el[0] is None:
1512 dl.append(el[1])
1513 else:
1514 dl.append(el[0] + ':' + el[1])
1515 if (varSpec['allocatable'] and not all(d == ':' for d in dl)) or \
1516 (any(d == ':' for d in dl) and not varSpec['allocatable']):
1517 if not implicitDeclaration:
1518 raise PYFTError('Missing dim are mandatory and allowed ' +
1519 'only for allocatable arrays')
1520 stmt += ', '.join(dl) + ')'
1521 if varSpec['allocatable']:
1522 stmt += ", ALLOCATABLE"
1523 if varSpec['parameter']:
1524 stmt += ", PARAMETER"
1525 if varSpec['i'] is not None:
1526 stmt += ", INTENT(" + varSpec['i'] + ")"
1527 if varSpec['opt'] is True:
1528 stmt += ", OPTIONAL"
1529 stmt += " :: " + varSpec['n']
1530 if varSpec['init'] is not None:
1531 stmt += "=" + varSpec['init']
1532 return stmt
1533
1534 @debugDecor
1535 def findIndexArrayBounds(self, arr, index, loopVar):
1536 """
1537 Find bounds and loop variable for a given array index
1538 :param arr: array node (named-E node with a array-R child)
1539 :param index: index of the rank of the array
1540 :param loopVar: None to create new variable for each added DO loop
1541 or a function that return the name of the variable to use for the
1542 loop control.
1543 This function returns a string (name of the variable), or True to create
1544 a new variable, or False to not transform this statement
1545 The functions takes as arguments:
1546 - lower and upper bounds as defined in the declaration statement
1547 - lower and upper bounds as given in the statement
1548 - name of the array
1549 - index of the rank
1550 :return: the tuple (loopName, lowerBound, upperBound) where:
1551 loopName is the name of the variable to use for the loop
1552 lower and upper bounds are the bounds to use for the DO loop
1553 loopName can be:
1554 - a string
1555 - False to discard this index
1556 - True to create a new variable to loop with
1557 """
1558 name = n2name(arr.find('./{*}N'))
1559 ss = arr.findall('./{*}R-LT/{*}array-R/{*}section-subscript-LT/{*}section-subscript')[index]
1560 # We are only interested by the subscript containing ':'
1561 # we must not iterate over the others, eg: X(:,1)
1562 if ':' in alltext(ss):
1563 # Look for lower and upper bounds for iteration and declaration
1564 lowerUsed = ss.find('./{*}lower-bound')
1565 upperUsed = ss.find('./{*}upper-bound')
1566 varDesc = self.varList.findVar(name, array=True)
1567 if varDesc is not None:
1568 lowerDecl, upperDecl = varDesc['as'][index]
1569 if lowerDecl is None:
1570 lowerDecl = '1' # default lower index for FORTRAN arrays
1571 else:
1572 lowerDecl, upperDecl = None, None
1573
1574 # name of the loop variable
1575 if loopVar is None:
1576 # loopVar is not defined, we create a new variable for the loop only if lower
1577 # and upper bounds have been found (easy way to discard character strings)
1578 varName = lowerDecl is not None and upperDecl is not None
1579 else:
1580 varName = loopVar(lowerDecl, upperDecl,
1581 None if lowerUsed is None else alltext(lowerUsed),
1582 None if upperUsed is None else alltext(upperUsed), name, index)
1583 return (varName,
1584 lowerDecl if lowerUsed is None else alltext(lowerUsed),
1585 upperDecl if upperUsed is None else alltext(upperUsed))
1586 return None
1587
1588 @debugDecor
1589 def arrayR2parensR(self, namedE, table):
1590 """
1591 Transform a array-R into a parens-R node by replacing slices by variables
1592 In 'A(:)', the ':' is in a array-R node whereas in 'A(JL)', 'JL' is in a parens-R node.
1593 Both the array-R and the parens-R nodes are inside a R-LT node
1594 :param namedE: a named-E node
1595 :param table: dictionnary returned by the decode function
1596 :param varList: None or a VarList object in which varaibles are searched for
1597 """
1598 # Before A(:): <f:named-E>
1599 # <f:N><f:n>A</f:n></f:N>
1600 # <f:R-LT>
1601 # <f:array-R>(
1602 # <f:section-subscript-LT>
1603 # <f:section-subscript>:</f:section-subscript>
1604 # </f:section-subscript-LT>)
1605 # </f:array-R>
1606 # </f:R-LT>
1607 # </f:named-E>
1608 # After A(I): <f:named-E>
1609 # <f:N><f:n>A</f:n></f:N>
1610 # <f:R-LT>
1611 # <f:parens-R>(
1612 # <f:element-LT>
1613 # <f:element><f:named-E><f:N><f:n>I</f:n></f:N></f:named-E></f:element>
1614 # </f:element-LT>)
1615 # </f:parens-R>
1616 # </f:R-LT>
1617 # </f:named-E>
1618
1619 nodeRLT = namedE.find('./{*}R-LT')
1620 arrayR = nodeRLT.find('./{*}array-R') # Not always in first position, eg: ICED%XRTMIN(:)
1621 if arrayR is not None:
1622 index = list(nodeRLT).index(arrayR)
1623 parensR = createElem('parens-R', text='(', tail=')')
1624 elementLT = createElem('element-LT')
1625 parensR.append(elementLT)
1626 ivar = -1
1627 for ss in nodeRLT[index].findall('./{*}section-subscript-LT/{*}section-subscript'):
1628 element = createElem('element', tail=', ')
1629 elementLT.append(element)
1630 if ':' in alltext(ss):
1631 ivar += 1
1632 varName = list(table.keys())[ivar] # variable name
1633 lower = ss.find('./{*}lower-bound')
1634 upper = ss.find('./{*}upper-bound')
1635 if lower is not None:
1636 lower = alltext(lower)
1637 if upper is not None:
1638 upper = alltext(upper)
1639 if lower is not None and ss.text is not None and ':' in ss.text:
1640 # fxtran bug workaround
1641 upper = lower
1642 lower = None
1643 if lower is None and upper is None:
1644 # E.g. 'A(:)'
1645 # In this case we use the DO loop bounds without checking validity with
1646 # respect to the array declared bounds
1647 element.append(createExprPart(varName))
1648 else:
1649 # E.g.:
1650 # !$mnh_expand_array(JI=2:15)
1651 # A(2:15) or A(:15) or A(2:)
1652 if lower is None:
1653 # lower bound not defined, getting lower declared bound for this array
1654 lower = self.varList.findVar(n2name(namedE.find('{*}N')),
1655 array=True)['as'][ivar][0]
1656 if lower is None:
1657 lower = '1' # default fortran lower bound
1658 elif upper is None:
1659 # upper bound not defined, getting upper declared bound for this array
1660 upper = self.varList.findVar(n2name(namedE.find('{*}N')),
1661 array=True)['as'][ivar][1]
1662 # If the DO loop starts from JI=I1 and goes to JI=I2; and array
1663 # bounds are J1:J2
1664 # We compute J1-I1+JI and J2-I2+JI and they should be the same
1665 # E.g: array bounds could be 'I1:I2' (becoming JI:JI) or 'I1+1:I2+1"
1666 # (becoming JI+1:JI+1)
1667 newlower = simplifyExpr(lower, add=varName, sub=table[varName][0])
1668 newupper = simplifyExpr(upper, add=varName, sub=table[varName][1])
1669 if newlower != newupper:
1670 raise PYFTError(("Don't know how to do with an array declared with " +
1671 "'{la}:{ua}' and a loop from '{ll}' to '{ul}'"
1672 ).format(la=lower, ua=upper,
1673 ll=table[varName][0],
1674 ul=table[varName][1]))
1675 element.append(createExprPart(newlower))
1676 else:
1677 element.append(ss.find('./{*}lower-bound'))
1678 element.tail = None # last element
1679 nodeRLT.remove(nodeRLT[index])
1680 nodeRLT.insert(index, parensR)
1681
1682 @debugDecor
1683 def findArrayBounds(self, arr, loopVar, extraVarList=None):
1684 """
1685 Find bounds and loop variable given an array
1686 :param arr: array node (named-E node with a array-R child)
1687 :param loopVar: None to create new variable for each added DO loop
1688 or a function that return the name of the variable to use for the loop
1689 control.
1690 This function returns a string (name of the variable), or True to create
1691 a new variable, or False to not transform this statement
1692 The functions takes as arguments:
1693 - lower and upper bounds as defined in the declaration statement
1694 - lower and upper bounds as given in the statement
1695 - name of the array
1696 - index of the rank
1697 :param extraVarList: None or list of variables (such as those contained in a VarList object)
1698 defined but not yet available in the self.varList object.
1699 :return: the tuple (table, newVar) where:
1700 table is a dictionnary: keys are loop variable names
1701 values are tuples with lower and upper bounds
1702 newVar is a list of loop variables not found in varList. This list has the same
1703 format as the varList list.
1704
1705 In case the loop variable cannot be defined, the function returns (None, [])
1706 """
1707 table = {} # ordered since python 3.7
1708 name = n2name(arr.find('./{*}N'))
1709 varNew = []
1710 extraVarList = extraVarList if extraVarList is not None else []
1711
1712 # Iteration on the different subscript
1713 for iss, ss in enumerate(arr.findall('./{*}R-LT/{*}array-R/{*}section-subscript-LT/' +
1714 '{*}section-subscript')):
1715 # We are only interested by the subscript containing ':'
1716 # we must not iterate over the others, eg: X(:,1)
1717 if ':' in alltext(ss):
1718 # Look for loop variable name and lower/upper bounds for iteration
1719 varName, lower, upper = self.findIndexArrayBounds(arr, iss, loopVar)
1720 # varName can be a string (name to use), True (to create a variable),
1721 # False (to discard the array
1722 if varName is not False and varName in table:
1723 raise PYFTError(("The variable {var} must be used for the rank #{i1} whereas " +
1724 "it is already used for rank #{i2} (for array {name})."
1725 ).format(var=varName, i1=str(iss),
1726 i2=str(list(table.keys()).index(varName)),
1727 name=name))
1728 if varName is True:
1729 # A new variable must be created
1730 # We look for a variable name that don't already exist
1731 # We can reuse a newly created varaible only if it is not used for the previous
1732 # indexes of the same statement
1733 j = 0
1734 found = False
1735 while not found:
1736 j += 1
1737 varName = 'J' + str(j)
1738 var = self.varList.findVar(varName, extraVarList=extraVarList + varNew)
1739 if (var is None or var.get('new', False)) and varName not in table:
1740 found = True
1741 varDesc = {'as': [], 'asx': [], 'n': varName, 'i': None,
1742 't': 'INTEGER', 'arg': False, 'use': False, 'opt': False,
1743 'scopePath': self.path}
1744 if varDesc not in varNew:
1745 varNew.append(varDesc)
1746
1747 elif (varName is not False and
1748 self.varList.findVar(varName, array=False, exactScope=True) is None):
1749 # We must declare the variable
1750 varDesc = {'as': [], 'asx': [], 'n': varName, 'i': None,
1751 't': 'INTEGER', 'arg': False, 'use': False, 'opt': False,
1752 'scopePath': self.path}
1753 varNew.append(varDesc)
1754
1755 # fill table
1756 table[varName] = (lower, upper)
1757
1758 return (None, []) if False in table else (table, varNew)
1759
1760 @debugDecor
1761 @updateVarList
1762 def renameVar(self, oldName, newName):
1763 """
1764 :param oldName: old name of the variable
1765 :param newName: new name of the variable
1766 """
1767 for node in self.findall('.//{*}N'):
1768 if n2name(node).upper() == oldName.upper():
1769 # Remove all n tag but one
1770 for nnn in node.findall('./{*}n')[1:]:
1771 node.remove(nnn)
1772 # Fill the first n with the new name
1773 node.find('./{*}n').text = newName
1774
1775 @debugDecor
1776 def removeVarIfUnused(self, varList, excludeDummy=False, excludeModule=False, simplify=False):
1777 """
1778 :param varList: list of variables to remove if unused. Each item is a list or tuple of two
1779 elements.
1780 The first one describes where the variable is used, the second one is
1781 the name of the variable. The first element is a '/'-separated path with
1782 each element having the form 'module:<name of the module>',
1783 'sub:<name of the subroutine>' or 'func:<name of the function>'
1784 :param excludeDummy: if True, dummy arguments are always kept untouched
1785 :param excludeModule: if True, module variables are always kept untouched
1786 :param simplify: try to simplify code (if we delete a declaration statement that used a
1787 variable as kind selector, and if this variable is not used else where,
1788 we also delete it)
1789 :return: the varList without the unremovable variables
1790 If possible, remove the variable from declaration, and from the argument list if needed
1791 """
1792 varList = self._normalizeUniqVar(varList)
1793 if excludeModule:
1794 varList = [v for v in varList if v[0].split('/')[-1].split(':')[0] != 'module']
1795
1796 varUsed = self.isVarUsed(varList, dummyAreAlwaysUsed=excludeDummy)
1797 varListToRemove = []
1798 for scopePath, varName in varList:
1799 assert scopePath.split('/')[-1].split(':')[0] != 'type', \
1800 "The removeVarIfUnused cannot be used with type members"
1801 if not varUsed[(scopePath, varName)]:
1802 varListToRemove.append([scopePath, varName])
1803 self.removeVar(varListToRemove, simplify=simplify)
1804 return varListToRemove
1805
1806 @debugDecor
1807 def isVarUsed(self, varList, exactScope=False, dummyAreAlwaysUsed=False):
1808 """
1809 :param varList: list of variables to test. Each item is a list or tuple of two elements.
1810 The first one describes where the variable is declared, the second one is
1811 the name of the variable. The first element is a '/'-separated path with
1812 each element having the form 'module:<name of the module>',
1813 'sub:<name of the subroutine>' or 'func:<name of the function>'
1814 :param exactScope: True to search strictly in scope
1815 :param dummyAreAlwaysUsed: Returns True if variable is a dummy argument
1816 :return: a dict whose keys are the elements of varList, and values are True when the
1817 variable is used, False otherwise
1818
1819 If exactScope is True, the function will search for variable usage
1820 only in this scope. But this feature has a limited interest.
1821
1822 If exactScope is False:
1823 - if scopePath is a subroutine/function in a contains section,
1824 and if the variable is not declared in this scope, usages are
1825 searched in the module/subroutine/function upper that declared
1826 the variable and in all subroutines/functions in the contains section
1827 - if scopePath is a module/subroutine/function that has a
1828 contains sections, usages are searched in all subroutines/functions
1829 in the contains section
1830
1831 To know if a variable can be removed, you must use exactScope=False
1832 """
1833 varList = self._normalizeUniqVar(varList)
1834 # We must not limit to self.getScopes because var can be used upper than self
1835 allScopes = {scope.path: scope for scope in self.mainScope.getScopes()}
1836
1837 # Computes in which scopes variable must be searched
1838 if exactScope:
1839 locsVar = {(scopePath, varName): [scopePath]
1840 for scopePath, varName in varList}
1841 else:
1842 locsVar = {}
1843 for scopePath, varName in varList:
1844 # We search everywhere if var declaration is not found
1845 # Otherwise, we search from the scope where the variable is declared
1846 var = allScopes[scopePath].varList.findVar(varName)
1847 path = scopePath.split('/')[0] if var is None else var['scopePath']
1848
1849 # We start search from here but we must include all routines in contains
1850 # that do not declare again the same variable name
1851 testScopes = [path] # we must search in the current scope
1852 for scPath, sc in allScopes.items():
1853 if scPath.startswith(path + '/') and \
1854 scPath.split('/')[-1].split(':')[0] != 'type':
1855 # sc is a scope path contained inside path and is not a type declaration
1856 if sc.varList.findVar(varName, exactScope=True) is None:
1857 # There is not another variable with same name declared inside
1858 testScopes.append(scPath) # if variable is used here, it is used
1859 locsVar[(scopePath, varName)] = testScopes
1860
1861 # For each scope to search, list all the variables used
1862 usedVar = {}
1863 for scopePath in list(set(item for sublist in locsVar.values() for item in sublist)):
1864 usedVar[scopePath] = []
1865 # Loop on all child in the scope
1866 for node in allScopes[scopePath]:
1867 # we don't want use statement, it could be where the variable is declared,
1868 # not a usage place
1869 if not tag(node) == 'use-stmt':
1870 if tag(node) == 'T-decl-stmt':
1871 # We don't want the part with the list of declared variables, we only want
1872 # to capture variables used in the kind selector or in the shape
1873 # specification
1874 nodesN = node.findall('.//{*}_T-spec_//{*}N') + \
1875 node.findall('.//{*}shape-spec//{*}N')
1876 else:
1877 nodesN = node.findall('.//{*}N')
1878
1879 # We look for the variable name in these 'N' nodes.
1880 for nodeN in nodesN:
1881 if dummyAreAlwaysUsed:
1882 # No need to if the variable is a dummy argument; because if it is
1883 # one it will be found in the argument list of the subroutine/function
1884 # and will be considered as used
1885 usedVar[scopePath].append(n2name(nodeN).upper())
1886 else:
1887 parPar = allScopes[scopePath].getParent(nodeN, 2) # parent of parent
1888 # We exclude dummy argument list to really check if the variable is used
1889 # and do not only appear as an argument of the subroutine/function
1890 if parPar is None or not tag(parPar) == 'dummy-arg-LT':
1891 usedVar[scopePath].append(n2name(nodeN).upper())
1892
1893 result = {}
1894 for scopePath, varName in varList:
1895 assert scopePath.split('/')[-1].split(':')[0] != 'type', \
1896 'We cannot check type component usage'
1897 result[(scopePath, varName)] = any(varName.upper() in usedVar[scopePath]
1898 for scopePath in locsVar[(scopePath, varName)])
1899
1900 return result
1901
1902 @debugDecor
1903 @updateVarList
1904 @updateTree('signal')
1905 def addONLY(self, parserOptions=None, wrapH=False):
1906 """
1907 Adds missing ONLY clause to USE statements
1908 :param parserOptions, wrapH: see the PYFT class
1909 """
1910 for useStmt in self.findall('.//{*}use-stmt'):
1911 module = useStmt.find('.//{*}module-N')
1912 if module.tail is None or module.tail.replace(' ', '').upper() != ',ONLY:':
1913 # A USE statement without the ONLY clause has been found
1914 modulename = n2name(module.find('.//{*}N')).upper()
1915 modulescope = 'module:' + modulename
1916 modulefile = self.tree.scopeToFiles(modulescope)
1917 if len(modulefile) != 1:
1918 raise PYFTError(f'No or several files define the {modulename} module')
1919 symbols = []
1920 pft = None
1921 try:
1922 # Open file to get the symbol list defined in the module
1923 if self.getFileName() == os.path.normpath(modulefile[0]):
1924 # interface declared in same file
1925 xml = self.mainScope
1926 pft = None
1927 else:
1929 modulefile[0], parserOptions, wrapH, tree=self.tree,
1930 clsPYFT=self._mainScope.__class__)
1931 xml = pft
1932
1933 # variable list
1934 symbols.extend(v['n'] for v in xml.varList.restrict(modulescope, True))
1935 # subroutine, functions and interfaces
1936 for scope in xml.getScopeNode(modulescope,
1937 excludeContains=False).getScopes(
1938 level=2,
1939 excludeContains=False,
1940 includeItself=False):
1941 if scope.path.split('/')[1].split(':')[0] == 'interface':
1942 if scope.path.split('/')[1].split(':')[1] != '--UNKNOWN--':
1943 # Named interface is a symbol
1944 symbols.append(scope.path.split('/')[1].split(':')[1])
1945 if len(scope.path.split('/')) == 3:
1946 # Subroutine and functions defined in the interface
1947 symbols.append(scope.path.split('/')[2].split(':')[1])
1948 else:
1949 symbols.append(scope.path.split('/')[1].split(':')[1])
1950 symbols = sorted(set(symbols))
1951 # Add all the symbols in the ONLY clause
1952 # <f:use-stmt>USE <f:module-N><f:N><f:n>MODNAME</f:n></f:N></f:module-N>, ONLY:
1953 # <f:rename-LT>
1954 # <f:rename><f:use-N><f:N><f:n>S1</f:n></f:N></f:use-N></f:rename>,
1955 # <f:rename><f:use-N><f:N><f:n>S2</f:n></f:N></f:use-N></f:rename>
1956 # </f:rename-LT></f:use-stmt>
1957 module.tail = ', ONLY: '
1958 renameLT = createElem('rename-LT')
1959 useStmt.append(renameLT)
1960 rename = None
1961 parent = self.getScopePath(useStmt)
1962 isVarUsed = self.isVarUsed([(parent, symbol.upper()) for symbol in symbols])
1963 for symbol in symbols:
1964 if isVarUsed[(parent, symbol.upper())]:
1965 useN = createElem('use-N', childs=createElem('N',
1966 childs=createElem('n', text=symbol)))
1967 rename = createElem('rename', tail=', ', childs=useN)
1968 renameLT.append(rename)
1969 if rename is None:
1970 # Module is unused, we suppress it
1971 previous = self.getSiblings(useStmt, before=True, after=False)
1972 previous = None if len(previous) == 0 else previous[-1]
1973 if previous is not None and useStmt.tail is not None:
1974 if previous.tail is None:
1975 previous.tail = ''
1976 previous.tail += useStmt.tail
1977 self.getParent(useStmt).remove(useStmt)
1978 self.tree.signal(self) # Tree must be updated
1979 else:
1980 # Remove the comma after the last child
1981 rename.tail = None
1982 finally:
1983 if pft is not None:
1984 pft.close()
1985
1986 @debugDecor
1987 @updateVarList
1988 def addArgInTree(self, varName, declStmt, pos, stopScopes, moduleVarList=None,
1989 otherNames=None,
1990 parserOptions=None, wrapH=False):
1991 """
1992 Adds an argument to the routine and propagates it upward until we encounter a scope
1993 where the variable exists or a scope in stopScopes
1994 :param varName: variable name
1995 :param declStmt: declarative statment (will be used by addVar)
1996 :param pos: position of the variable in the list of dummy argument
1997 :param stopScopes: list of scopes to reach
1998 :param moduleVarList: list of module variable specification to insert in the xml code
1999 a module variable specification is a list of two elements:
2000 - module name
2001 - variable name or or list of variable names
2002 or None to add a USE statement without the ONLY attribute
2003 use moduleVarList to not add module variables
2004 :param otherNames: None or list of other variable names that can be used
2005 These variables are used first
2006 :param parserOptions, wrapH: see the PYFT class
2007
2008 Argument is inserted only on paths leading to one of scopes listed in stopScopes
2009 """
2010 def insertInArgList(varName, varNameToUse, pos, callFuncStmt):
2011 """
2012 Insert varName in the list of arguments to the subroutine or function call
2013 :param varName: name of the dummy argument
2014 :param varNameToUse: name of the variable
2015 :param pos: inclusion position
2016 :param callFuncStmt: call statement or function call
2017 """
2018 argList = callFuncStmt.find('./{*}R-LT/{*}parens-R/{*}element-LT')
2019 if argList is not None:
2020 container = createElem('element')
2021 else:
2022 argList = callFuncStmt.find('./{*}arg-spec')
2023 container = createElem('arg')
2024 if argList is None:
2025 # Call without argument
2026 callFuncStmt.find('./{*}procedure-designator').tail = '('
2027 argList = createElem('arg-spec', tail=')')
2028 callFuncStmt.append(argList)
2029 item = createExprPart(varNameToUse)
2030 previous = pos - 1 if pos >= 0 else len(argList) + pos # convert negative pos
2031 while previous >= 0 and tag(argList[previous]) in ('C', 'cnt'):
2032 previous -= 1
2033 following = pos if pos > 0 else len(argList) + pos + 1 # convert negative pos
2034 while following <= len(argList) - 1 and tag(argList[following]) in ('C', 'cnt'):
2035 following += 1
2036 if (previous >= 0 and argList[previous].find('./{*}arg-N/{*}k') is not None) or \
2037 (following <= len(argList) - 1 and
2038 argList[following].find('./{*}arg-N/{*}k') is not None) or \
2039 following == len(argList):
2040 # We use the key=val syntax whereever it is possible because it's safer in case of
2041 # optional arguments
2042 # If previous arg, or following arg is already with a key=val syntax, we can (must)
2043 # use it
2044 # If the inserted argument is the last one of the list, it also can use this syntax
2045 k = createElem('k', text=varName)
2046 argN = createElem('arg-N', tail='=')
2047 argN.append(k)
2048 argN.set('n', varName)
2049 container.append(argN)
2050 container.append(item)
2051 self.insertInList(pos, container, argList)
2052
2053 if self.path in stopScopes or self.tree.isUnderStopScopes(self.path, stopScopes):
2054 # We are on the path to a scope in the stopScopes list, or scopeUp is one of the
2055 # stopScopes
2056 var = self.varList.findVar(varName, exactScope=True)
2057 if otherNames is not None:
2058 vOther = [self.varList.findVar(v, exactScope=True) for v in otherNames]
2059 vOther = [v for v in vOther if v is not None]
2060 if len(vOther) > 0:
2061 var = vOther[-1]
2062
2063 if var is None:
2064 # The variable doesn't exist in this scope, we add it
2065 self.addVar([[self.path, varName, declStmt, pos]])
2066 if moduleVarList is not None:
2067 # Module variables must be added when var is added
2068 self.addModuleVar([(self.path, moduleName, moduleVarNames)
2069 for (moduleName, moduleVarNames) in moduleVarList])
2070
2071 # We look for interface declaration if subroutine is directly accessible
2072 if len(self.path.split('/')) == 1:
2073 filename, scopePathInterface = self.tree.findScopeInterface(self.path)
2074 if filename is not None:
2075 pft = None
2076 try:
2077 if self.getFileName() == os.path.normpath(filename):
2078 # interface declared in same file
2079 xml = self.mainScope
2080 pft = None
2081 else:
2083 filename, parserOptions, wrapH, tree=self.tree,
2084 clsPYFT=self._mainScope.__class__)
2085 xml = pft
2086 scopeInterface = xml.getScopeNode(scopePathInterface)
2087 varInterface = scopeInterface.varList.findVar(varName, exactScope=True)
2088 if varInterface is None:
2089 scopeInterface.addVar([[scopePathInterface, varName,
2090 declStmt, pos]])
2091 if moduleVarList is not None:
2092 # Module variables must be added when var is added
2093 xml.addModuleVar(
2094 [(scopePathInterface, moduleName, moduleVarNames)
2095 for (moduleName, moduleVarNames) in moduleVarList])
2096 if pft is not None:
2097 pft.write()
2098 finally:
2099 if pft is not None:
2100 pft.close()
2101
2102 if var is None and self.path not in stopScopes:
2103 # We must propagates upward
2104 # scopes calling the current scope
2105 for scopePathUp in self.tree.calledByScope(self.path):
2106 if scopePathUp in stopScopes or self.tree.isUnderStopScopes(scopePathUp,
2107 stopScopes):
2108 # We are on the path to a scope in the stopScopes list, or scopePathUp is
2109 # one of the stopScopes
2110 # can be defined several times?
2111 for filename in self.tree.scopeToFiles(scopePathUp):
2112 pft = None
2113 try:
2114 if self.getFileName() == os.path.normpath(filename):
2115 # Upper scope is in the same file
2116 xml = self.mainScope
2117 pft = None
2118 else:
2120 filename, parserOptions, wrapH,
2121 tree=self.tree,
2122 clsPYFT=self._mainScope.__class__)
2123 xml = pft
2124 scopeUp = xml.getScopeNode(scopePathUp)
2125 # Add the argument and propagate upward
2126 scopeUp.addArgInTree(
2127 varName, declStmt, pos,
2128 stopScopes, moduleVarList, otherNames,
2129 parserOptions=parserOptions,
2130 wrapH=wrapH)
2131 # Add the argument to calls (subroutine or function)
2132 name = self.path.split('/')[-1].split(':')[1].upper()
2133 isCalled = False
2134 varNameToUse = varName
2135 if otherNames is not None:
2136 vOther = [scopeUp.varList.findVar(v, exactScope=True)
2137 for v in otherNames]
2138 vOther = [v for v in vOther if v is not None]
2139 if len(vOther) > 0:
2140 varNameToUse = vOther[-1]['n']
2141 if self.path.split('/')[-1].split(':')[0] == 'sub':
2142 # We look for call statements
2143 for callStmt in scopeUp.findall('.//{*}call-stmt'):
2144 callName = n2name(callStmt.find(
2145 './{*}procedure-designator/{*}named-E/{*}N')).upper()
2146 if callName == name:
2147 insertInArgList(varName, varNameToUse, pos, callStmt)
2148 isCalled = True
2149 else:
2150 # We look for function use
2151 for funcCall in scopeUp.findall(
2152 './/{*}named-E/{*}R-LT/{*}parens-R/' +
2153 '{*}element-LT/../../..'):
2154 funcName = n2name(funcCall.find('./{*}N')).upper()
2155 if funcName == name:
2156 insertInArgList(varName, varNameToUse, pos, funcCall)
2157 isCalled = True
2158 if pft is not None:
2159 pft.write()
2160 finally:
2161 if pft is not None:
2162 pft.close()
2163
2164 if isCalled:
2165 # We must check in the scope (or upper scopes) if an interface
2166 # block declares the routine
2167 for interface in self.findall('.//{*}interface-construct/{*}' +
2168 'program-unit/{*}subroutine-stmt/' +
2169 '{*}subroutine-N/{*}N/../../../'):
2170 if n2name(interface.find('./{*}subroutine-stmt/' +
2171 '{*}subroutine-N/' +
2172 '{*}N')).upper() == name:
2173 # We must add the argument to the interface
2174 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)
varSpec2stmt(varSpec, implicitDeclaration=False)
addModuleVar(self, moduleVarList)
Definition variables.py:878
checkUnusedLocalVar(self, mustRaise=False, excludeList=None)
Definition variables.py:995
addArrayParenthesesInNode(self, node)
removeArrayParenthesesInNode(self, node)
checkIntent(self, mustRaise=False)
Definition variables.py:541
modifyAutomaticArrays(self, declTemplate=None, startTemplate=None, endTemplate=None)
checkONLY(self, mustRaise=False)
Definition variables.py:572
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:607
conservativePYFT(filename, parserOptions, wrapH, tree=None, verbosity=None, clsPYFT=None)
Definition pyfortool.py:37
_getDeclStmtTag(scopePath)
Definition variables.py:46