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