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