xref: /petsc/share/petsc/chkerrconvert.py (revision 337345a6b046b502f0f6684b816b8d0e16228167)
1#!/usr/bin/env python3
2"""
3# Created: Wed Feb 23 17:32:36 2022 (-0600)
4# @author: jacobfaibussowitsch
5"""
6import sys
7if sys.version_info < (3,5):
8  raise RuntimeError('requires python 3.5')
9import os
10import re
11import subprocess
12import pathlib
13import collections
14import itertools
15
16class Replace:
17  __slots__ = 'verbose','special'
18
19  def __init__(self,verbose,special=None):
20    """
21    verbose: (bool)                verbosity level
22    special: (set-like of strings) list of functions/symbols that will remain untouched
23    """
24    self.verbose = bool(verbose)
25    if special is None:
26      special = {
27        'PetscOptionsBegin','PetscObjectOptionsBegin','PetscOptionsEnd',
28        'MatPreallocateInitialize','MatPreallocateFinalize',
29        'PetscDrawCollectiveBegin','PetscDrawCollectiveEnd'
30      }
31    self.special = special
32    return
33
34  def __call__(self,match):
35    """
36    match: a match object from re.match containing 2 or 3 groups
37    """
38    if any(map(match.group(0).__contains__,self.special)):
39      if self.verbose:
40        print('SKIPPED',match.group(0))
41      return match.group(0)
42    ierr,chkerr = match.group(1),match.group(2)
43    chkerr_suff = chkerr.replace('CHKERR','')
44    replace     = 'PetscCall'
45    if chkerr_suff == 'Q':
46      pass
47    elif chkerr_suff == 'V':
48      replace += 'Void'
49    elif chkerr_suff in {'ABORT','CONTINUE'}:
50      replace += chkerr_suff.title()
51      if chkerr_suff == 'ABORT':
52        comm = match.group(3).split(',')[0]
53        ierr = ','.join((comm,ierr))
54    elif chkerr_suff == 'XX':
55      replace += 'Throw'
56    else:
57      replace += chkerr_suff
58    return '{}({})'.format(replace,ierr)
59
60class Processor:
61  __slots__ = (
62    'chkerr_re','pinit_re','pfinal_re','retierr_re','cleanup_re','edecl_re','euses_re',
63    'addcount','delcount','verbose','dry_run','del_empty_last_line','replace_chkerrs'
64  )
65
66  def __init__(self,verbose,dry_run,del_empty_last_line):
67    """
68    verbose:             (int)  verbosity level
69    dry_run:             (bool) is this a dry-run
70    del_empty_last_line: (bool) should we try and delete empty last (double) lines in the file
71    """
72    self.chkerr_re  = re.compile(r'(?:\w+\s+)?\w*(?:err|stat|ccer)\w*\s*=\s*(.*?)\s*;\s*(CHKERR.*)\((.*?)\)')
73    self.pinit_re   = re.compile(r'(?:\w+\s+)?ierr\s*=\s*(PetscInitialize.*);\s*if\s+\(ierr\)\s*return\s+ierr.*')
74    self.pfinal_re  = re.compile(r'(?:\w+\s+)?ierr\s*=\s*(PetscFinalize.*)\s*;.*')
75    self.retierr_re = re.compile(r'(?:\w+\s+)?(return)\s+ierr\s*;.*')
76    self.cleanup_re = re.compile(r'{\s*(PetscCall[^;]*;)\s*}')
77    self.edecl_re   = re.compile(r'\s*PetscErrorCode\s+ierr\s*;.*')
78    self.euses_re   = re.compile(r'.*ierr\s*=\s*.*')
79    self.addcount   = 0
80    self.delcount   = 0
81    self.verbose    = verbose
82    self.dry_run    = dry_run
83
84    self.del_empty_last_line = del_empty_last_line
85    self.replace_chkerrs     = Replace(verbose > 2)
86    return
87
88  def __call__(self,path):
89    new_lines,changes    = [],[]
90    last                 = collections.deque(('',''),maxlen=2)
91    error_code_decls     = []
92    error_code_uses      = []
93    petsc_finalize_found = False
94    delete_set           = set()
95    is_fortran_binding   = any(p.startswith('ftn-') for p in path.parts)
96
97    for lineno,line in enumerate(path.read_text().splitlines()):
98      if line.lstrip().startswith('PetscFunctionBegin') and last[0] == '' and last[1] == '{':
99        # found
100        # {
101        #   <should delete this empty line>
102        #   PetscFunctionBegin;
103        delete_set.add(lineno-1)
104        changes.append((lineno,last[0],None))
105      # check for trivial unused variable
106      if self.euses_re.match(line):
107        error_code_uses.append((line,lineno))
108      if self.edecl_re.match(line):
109        error_code_decls.append((line,lineno))
110      # check for PetscInitialize() to wrap
111      repl = self.pinit_re.sub(r'PetscCall(\1);',line)
112      if repl == line:
113        if is_fortran_binding:
114          petsc_finalize_found = False
115        else:
116          repl = self.pfinal_re.sub(r'PetscCall(\1);',line)
117        if repl == line:
118          repl = self.chkerr_re.sub(self.replace_chkerrs,line)
119          if petsc_finalize_found and repl == line:
120            repl = self.retierr_re.sub(r'\1 0;',line)
121          petsc_finalize_found = False
122        else:
123          petsc_finalize_found = True
124      if repl != line:
125        repl = self.cleanup_re.sub(r'\1',repl)
126        self.add()
127        self.delete()
128        changes.append((lineno,line,repl))
129      new_lines.append(repl)
130      last.appendleft(line.strip())
131
132    self.delete_unused_error_code_decls(error_code_decls,error_code_uses,new_lines,delete_set,changes)
133
134    if len(new_lines) and new_lines[-1] == '':
135      if self.del_empty_last_line:
136        self.delete()
137        changes.append((len(new_lines),new_lines[-1],None))
138      else:
139        new_lines[-1] = '\n'
140
141    self.delete(len(delete_set))
142    if delete_set:
143      new_lines = [l for i,l in enumerate(new_lines) if i not in delete_set]
144
145    return new_lines,changes,delete_set
146
147
148  def delete_unused_error_code_decls(self,error_code_decls,error_code_uses,new_lines,delete_set,changes):
149    def pairwise(iterable,default=None):
150      "s -> (s0,s1,..s(n-1)), (s1,s2,.., sn), (s2, s3,..,s(n+1)), ..."
151      n      = 2
152      iters  = iter(iterable)
153      result = tuple(itertools.islice(iters,n))
154      if len(result) == n:
155        yield result
156      for elem in iters:
157        result = result[1:]+(elem,)
158        yield result
159      if default is not None:
160        yield result[-1],default
161
162
163    if not len(error_code_decls):
164      return # nothing to do
165
166    # see if we can find consecutive PetscErrorCode ierr; without uses, if so, delete
167    # them
168    default_entry = (None,len(new_lines))
169    for (cur_line,cur_lineno),(_,next_lineno) in pairwise(error_code_decls,default=default_entry):
170      line_range = range(cur_lineno,next_lineno)
171      if not any(ln in line_range for _,ln in error_code_uses):
172        # the ierr is unused
173        assert new_lines[cur_lineno] == cur_line # don't want to delete the wrong line
174        delete_set.add(cur_lineno)
175        if self.dry_run:
176          change = (cur_lineno,cur_line,None)
177          added  = False
178          for i,(cln,_,_) in enumerate(changes):
179            if cln > cur_lineno:
180              changes.insert(i,change)
181              added = True
182              break
183          if not added:
184            changes.append(change)
185    return
186
187  def add(self,n=1):
188    self.addcount += n
189    return
190
191  def delete(self,n=1):
192    self.delcount += n
193    return
194
195  def summary(self):
196    if self.verbose:
197      print(self.delcount,'deletion(s) and',self.addcount,'insertion(s)')
198    if self.delcount or self.addcount:
199      mod = 'found' if self.dry_run else 'made'
200      print(
201        'Insertions and/or deletions were',mod+', '
202        'suggest running the tool again until no more changes are',mod
203      )
204    return
205
206
207def path_resolve_strict(path):
208  path = pathlib.Path(path)
209  return path.resolve() if sys.version_info < (3,6) else path.resolve(strict=True)
210
211def subprocess_run(*args,**kwargs):
212  if sys.version_info < (3,7):
213    kwargs.setdefault('stdout',subprocess.PIPE)
214    kwargs.setdefault('stderr',subprocess.PIPE)
215  else:
216    kwargs.setdefault('capture_output',True)
217  return subprocess.run(args,**kwargs)
218
219def get_paths_list(start_path,search_tool,force):
220  if start_path.is_dir():
221    if search_tool == 'rg':
222      extra_flags = ['-T','fortran','--no-stats','-j','5']
223    else: # grep
224      extra_flags = ['-E','-r']
225
226    if force:
227      import glob
228      file_list = glob.iglob(str(start_path/'**'),recursive=True)
229    else:
230      ret = subprocess_run(search_tool,*extra_flags,'-l','CHKERR',str(start_path))
231      try:
232        ret.check_returncode()
233      except subprocess.CalledProcessError as cpe:
234        print('command:',ret.args)
235        print('stdout:\n',ret.stdout.decode())
236        print('stderr:\n',ret.stderr.decode())
237        raise RuntimeError from cpe
238      else:
239        file_list = ret.stdout.decode().splitlines()
240
241    filter_file = lambda x: x.endswith(('.c','.cpp','.cxx','.h','.hpp','.C','.H','.inl','.c++','.cu'))
242    found_list  = [x.resolve() for x in map(pathlib.Path,filter(filter_file,map(str,file_list)))]
243    found_list  = [f for f in found_list if not f.is_dir()]
244  else:
245    found_list = [start_path]
246
247  assert 'chkerrconvert.py' not in found_list
248  return found_list
249
250def main(search_tool,start_path,dry_run,verbose,force,del_empty_last_line):
251  if start_path == '${PETSC_DIR}/src':
252    try:
253      petsc_dir = os.environ['PETSC_DIR']
254    except KeyError as ke:
255      mess = 'Must either define PETSC_DIR as environment variable or pass it via flags to use '+start_path
256      raise RuntimeError(mess) from ke
257    start_path = path_resolve_strict(petsc_dir)/'src'
258  start_path = path_resolve_strict(start_path)
259  this_path  = path_resolve_strict(os.getcwd())
260
261  found_list = get_paths_list(start_path,search_tool,force)
262  processor  = Processor(verbose,dry_run,del_empty_last_line)
263
264  for path in found_list:
265    # check if this is a fortran binding
266    if any(p.startswith('ftn-auto') for p in path.parts):
267      if verbose > 2:
268        print('skipping',str(path),'because it is an auto-generated fortran binding')
269      continue # skip auto-generated files
270
271    if path.stem.endswith('feopencl'):
272      # the feopencl has some exceptions
273      continue
274
275    new_lines,changes,delete_set = processor(path)
276
277    if dry_run:
278      if len(changes):
279        print(str(path.relative_to(this_path))+':')
280        for lineno,line,repl in changes:
281          lineno += 1
282          print(f'{lineno}: - {line}')
283          if repl is not None:
284            print(f'{lineno}: + {repl}')
285    elif processor.delcount or processor.addcount:
286      output = '\n'.join(new_lines)
287      if not output.endswith('\n'):
288        output += '\n'
289      path.write_text(output)
290
291  processor.summary()
292  return
293
294
295if __name__ == '__main__':
296  import argparse
297  import signal
298
299  signal.signal(signal.SIGPIPE,signal.SIG_DFL) # allow the output of this script to be piped
300  parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
301  parser.add_argument('path',nargs='?',default='${PETSC_DIR}/src',metavar='<path>',help='path to directory base or file')
302  parser.add_argument('-s','--search-tool',default='rg',metavar='<executable>',choices=['rg','grep'],help='search tool to use to find files containing matches (rg or grep)')
303  parser.add_argument('-n','--dry-run',action='store_true',help='print what the result would be')
304  parser.add_argument('-v','--verbose',action='count',default=0,help='verbose')
305  parser.add_argument('-f','--force',action='store_true',help='don\'t narrow search using SEARCH TOOL, just replace everything under PATH')
306  parser.add_argument('--delete-empty-last-line',action='store_true',help='remove empty lines at the end of the file')
307
308  if len(sys.argv) == 1:
309    parser.print_help()
310  else:
311    args = parser.parse_args()
312    main(args.search_tool,args.path,args.dry_run,args.verbose,args.force,args.delete_empty_last_line)
313