#!/usr/bin/env python3 import sys import re import logging from collections import deque from io import StringIO # Declare various RE snippets for building larger REs # Valid fortran argument/variable name VALID_NAME = r"[a-zA-Z][a-zA-Z_0-9]*" # Set up valid type declaration DECL_KIND_KW = rf"\( (?:kind = )? (?:{VALID_NAME}|[0-9]+) \)".replace(" ", r"\s*") DECL_KIND_STAR = r"\s*\*\s*[0-9]+" CHAR_KIND_SPEC = rf"(?:(?:len|kind) = )? (?:{VALID_NAME}|[0-9]+|\*|:)" CHAR_KIND_SPEC = rf"\( {CHAR_KIND_SPEC}(?: , {CHAR_KIND_SPEC}) \)".replace(" ", r"\s*") DECL_KIND_SPEC = rf"\s*(?:{DECL_KIND_STAR}|{DECL_KIND_KW})" DECL_TYPE_DEF = rf"""(?: (?:integer|real|complex)(?:{DECL_KIND_SPEC})?| character(?:{DECL_KIND_STAR}|{CHAR_KIND_SPEC})?| logical(?:{DECL_KIND_KW})?|type|class)""" # String styles DQUOTED = r'"[^"]*"' SQUOTED = r"'[^']*'" QUOTED = rf"(?:{DQUOTED}|{SQUOTED})" # Stop at punctuation NOT_PUNC = r"[^()\"'\[\]]" NOT_PUNC_COMMA = r"[^()\"',\[\]]" NOT_BRAC = r"[^()]" # Enable new-style fortran arrays (square brackets) BRAC_OPEN = r"[(\[]" BRAC_CLOSE = r"[)\]]" NOT_PAREN = r"[^()]" # Nested parens for up to 3-level nesting (1st level's parens are outside this variable) # Should more than 3 levels be necessary, need to reduplicate this deeper. NESTED_PAREN_1TO3_CONTENTS = ( rf"(?:{NOT_PAREN}+|\((?:{NOT_PAREN}+|\({NOT_PAREN}*\))*\))*" ) # Variable statement VAR_RE = re.compile( rf""" \s*(?P{VALID_NAME}) \s*(?P(:?\((?P{NESTED_PAREN_1TO3_CONTENTS})\))? \s*(?:(?P=>?)\s*(?P(?: {NOT_PUNC_COMMA}+| {BRAC_OPEN}(?:{NOT_PUNC}|{BRAC_OPEN}{NOT_PUNC}*{BRAC_CLOSE}|{QUOTED})*{BRAC_CLOSE}| {QUOTED})+))?)? \s*(?:(?P,)|\n?) \s*""", re.IGNORECASE | re.VERBOSE, ) # Use statement USE_PARSE_RE = re.compile( rf"""\s* use(\s+|(?P\s*,\s*Intrinsic\s+::\s*)) (?P{VALID_NAME}) (?P\s*,\s*only\s*:)?\s* (?P.*)$""", flags=re.IGNORECASE | re.VERBOSE, ) COMMON_USES_RE = re.compile(r'^#include\s*"([^"]*(cp_common_uses.f90|base_uses.f90))"') LOCAL_NAME_RE = re.compile( rf"\s*(?P{VALID_NAME})(?:\s*=>\s*{VALID_NAME})?\s*$", re.VERBOSE ) VAR_DECL_RE = re.compile( rf""" \s*(?P{DECL_TYPE_DEF}) \s*(?P\({NESTED_PAREN_1TO3_CONTENTS}\))? \s*(?P(?:\s*,\s*[a-zA-Z_0-9]+(?:\s*\({NESTED_PAREN_1TO3_CONTENTS}\))?)+)?\s* (?P::)? (?P[^\n]+)\n? """, re.IGNORECASE | re.VERBOSE, ) INDENT_SIZE = 2 DECL_LINELENGTH = 100 DECL_OFFSET = 50 OMP_DIR_RE = re.compile(r"^\s*(!\$omp)", re.IGNORECASE) OMP_RE = re.compile(r"^\s*(!\$)", re.IGNORECASE) FCT_RE = re.compile( r"^([^\"'!]* )?FUNCTION\s+\w+\s*(\(.*\))?(\s*RESULT\s*\(\w+\))?\s*;?\s*$", re.IGNORECASE, ) SUBR_RE = re.compile( r"^([^\"'!]* )?SUBROUTINE\s+\w+\s*(\(.*\))?\s*;?\s*$", re.IGNORECASE ) END_RE = re.compile(r" *end\s*(?:subroutine|function)", re.IGNORECASE) START_ROUTINE_RE = re.compile( r"^([^\"'!]* )?(?Psubroutine|function) +(?P[a-zA-Z_][a-zA-Z_0-9]*) *(?:\((?P[^()]*)\))? *(?:result *\( *(?P[a-zA-Z_][a-zA-Z_0-9]*) *\))? *(?:bind *\([^()]+\))? *\n?", re.IGNORECASE, ) # $ typeBeginRe = re.compile( r" *(?Pinteger(?: *\* *[0-9]+)?|logical|character(?: *\* *[0-9]+)?|real(?: *\* *[0-9]+)?|complex(?: *\* *[0-9]+)?|type)[,( ]", re.IGNORECASE, ) attributeRe = re.compile( r" *, *(?P[a-zA-Z_0-9]+) *(?:\( *(?P(?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*)\))? *", re.IGNORECASE, ) IGNORE_RE = re.compile(r" *(?:|implicit +none *)$", re.IGNORECASE) INTERFACE_START_RE = re.compile(r" *interface *$", re.IGNORECASE) INTERFACE_END_RE = re.compile(r" *end +interface *$", re.IGNORECASE) INCLUDE_RE = re.compile(r"#? *include +[\"'](?P.+)[\"'] *$", re.IGNORECASE) CONTAINS_RE = re.compile(r" *contains *$", re.IGNORECASE) NON_WORD_RE = re.compile(r"(\(/|/\)|[^-+a-zA-Z0-9_.])") STR_RE = re.compile(r"('[^'\n]*'|\"[^\"\n]*\")") MODULE_N_RE = re.compile( r".*:: *moduleN *= *(['\"])[a-zA-Z_0-9]+\1", flags=re.IGNORECASE ) MODULE_RE = re.compile( r" *(?:module|program) +(?P[a-zA-Z_][a-zA-Z_0-9]*) *(?:!.*)?$", flags=re.IGNORECASE, ) class InputStreamError(Exception): pass class CharFilter(object): """ An iterator to wrap the iterator returned by `enumerate` and ignore comments and characters inside strings """ def __init__(self, it): self._it = it self._instring = "" def __iter__(self): return self def __next__(self): """python 3 version""" pos, char = next(self._it) if not self._instring and char == "!": raise StopIteration # detect start/end of a string if char == '"' or char == "'": if self._instring == char: self._instring = "" elif not self._instring: self._instring = char if self._instring: return self.__next__() return (pos, char) def next(self): """python 2 version""" pos, char = self._it.next() if not self._instring and char == "!": raise StopIteration # detect start/end of a string if char == '"' or char == "'": if self._instring == char: self._instring = "" elif not self._instring: self._instring = char if self._instring: return self.next() return (pos, char) class InputStream(object): """ Class to read logical Fortran lines from a Fortran file. """ def __init__(self, infile): self.line_buffer = deque([]) self.infile = infile self.line_nr = 0 def nextFortranLine(self): """Reads a group of connected lines (connected with &, separated by newline or semicolon) returns a touple with the joined line, and a list with the original lines. Doesn't support multiline character constants! """ lineRe = re.compile( # $ r"(?:(?P#.*\n?)| *(&)?(?P(?:!\$|[^&!\"']+|\"[^\"]*\"|'[^']*')*)(?P&)? *(?P!.*)?\n?)", re.IGNORECASE, ) joinedLine = "" comments = [] lines = [] continuation = 0 while True: if not self.line_buffer: line = self.infile.readline().replace("\t", 8 * " ") self.line_nr += 1 # convert OMP-conditional fortran statements into normal fortran statements # but remember to convert them back is_omp_conditional = False omp_indent = 0 if OMP_RE.match(line): omp_indent = len(line) - len(line.lstrip(" ")) line = OMP_RE.sub("", line, count=1) is_omp_conditional = True line_start = 0 for pos, char in CharFilter(enumerate(line)): if char == ";" or pos + 1 == len(line): self.line_buffer.append( omp_indent * " " + "!$" * is_omp_conditional + line[line_start : pos + 1] ) omp_indent = 0 is_omp_conditional = False line_start = pos + 1 if line_start < len(line): # line + comment self.line_buffer.append( "!$" * is_omp_conditional + line[line_start:] ) if self.line_buffer: line = self.line_buffer.popleft() if not line: break lines.append(line) m = lineRe.match(line) if not m or m.span()[1] != len(line): # FIXME: does not handle line continuation of # omp conditional fortran statements # starting with an ampersand. raise InputStreamError("unexpected line format:" + repr(line)) if m.group("preprocessor"): if len(lines) > 1: raise InputStreamError( "continuation to a preprocessor line not supported " + repr(line) ) comments.append(line) break coreAtt = m.group("core") if OMP_RE.match(coreAtt) and joinedLine.strip(): # remove omp '!$' for line continuation coreAtt = OMP_RE.sub("", coreAtt, count=1).lstrip() joinedLine = joinedLine.rstrip("\n") + coreAtt if coreAtt and not coreAtt.isspace(): continuation = 0 if m.group("continue"): continuation = 1 if line.lstrip().startswith("!") and not OMP_RE.search(line): comments.append(line.rstrip("\n")) elif m.group("comment"): comments.append(m.group("comment")) else: comments.append("") if not continuation: break return (joinedLine, comments, lines) def parseRoutine(inFile, logger): """Parses a routine""" routine = { "preRoutine": [], "core": [], "strippedCore": [], "begin": [], "end": [], "preDeclComments": [], "declarations": [], "declComments": [], "postDeclComments": [], "parsedDeclarations": [], "postRoutine": [], "kind": None, "name": None, "arguments": None, "result": None, "interfaceCount": 0, "use": [], } stream = InputStream(inFile) while True: jline, _, lines = stream.nextFortranLine() if len(lines) == 0: break if FCT_RE.match(jline) or SUBR_RE.match(jline): break routine["preRoutine"].extend(lines) m = INCLUDE_RE.match(lines[0]) if m: try: subF = open(m.group("file"), "r", encoding="utf8") subStream = InputStream(subF) while True: subjline, _, sublines = subStream.nextFortranLine() if not sublines: break routine["strippedCore"].append(subjline) subF.close() except Exception: import traceback logger.debug( "error trying to follow include '{}', this might lead to the removal of used variables".format( m.group("file") ) ) if logger.isEnabledFor(logging.DEBUG): traceback.print_exc() if jline: routine["begin"] = lines m = START_ROUTINE_RE.match(jline) if not m or m.span()[1] != len(jline): raise SyntaxError("unexpected subroutine start format:" + repr(lines)) routine["name"] = m.group("name") routine["kind"] = m.group("kind") if m.group("arguments") and m.group("arguments").strip(): routine["arguments"] = list( x.strip() for x in m.group("arguments").split(",") ) if m.group("result"): routine["result"] = m.group("result") if (not routine["result"]) and (routine["kind"].lower() == "function"): routine["result"] = routine["name"] while True: jline, comment_list, lines = stream.nextFortranLine() comments = "\n".join(_ for _ in comment_list) if len(lines) == 0: break if lines[0].lower().startswith("#include"): break if not IGNORE_RE.match(jline): if typeBeginRe.match(jline): if routine["postDeclComments"]: routine["declComments"].extend(routine["postDeclComments"]) routine["postDeclComments"] = [] if typeBeginRe.match(jline): m = VAR_DECL_RE.match(jline) if m.group("type").lower() == "type" and not m.group("parameters"): break if not m or m.span()[1] != len(jline): raise SyntaxError("unexpected type format:" + repr(jline)) decl = { "type": m.group("type"), "parameters": None, "attributes": [], "vars": [], } if m.group("parameters"): decl["parameters"] = ( m.group("parameters").replace(" ", "").replace(",", ", ") ) str = m.group("attributes") while str: m2 = attributeRe.match(str) if not m2: raise SyntaxError( "unexpected attribute format " + repr(str) + " in " + repr(lines) ) decl["attributes"].append( m2.group().replace(" ", "").replace(",", ", ")[2:] ) str = str[m2.span()[1] :] str = m.group("vars") while True: m2 = VAR_RE.match(str) if not m2: raise SyntaxError( "unexpected var format " + repr(str) + " in " + repr(lines) ) var = m2.group("var") if m2.group("param"): var += "(" + m2.group("param") + ")" if m2.group("value"): var += " {} ".format(m2["assignment"]) var += m2.group("value") decl["vars"].append(var) str = str[m2.span()[1] :] if not m2.group("continue"): if str: raise SyntaxError( "error parsing vars (leftover=" + repr(str) + ") in " + repr(lines) ) break routine["parsedDeclarations"].append(decl) elif INTERFACE_START_RE.match(jline): istart = lines interfaceDeclFile = StringIO() while True: jline, _, lines = stream.nextFortranLine() if INTERFACE_END_RE.match(jline): iend = lines break interfaceDeclFile.writelines(lines) interfaceDeclFile = StringIO(interfaceDeclFile.getvalue()) iroutines = [] while True: iroutine = parseRoutine(interfaceDeclFile, logger) if not iroutine["kind"]: if len(iroutines) == 0: interfaceDeclFile.seek(0) raise SyntaxError( "error parsing interface:" + repr(interfaceDeclFile.read()) ) iroutines[-1]["postRoutine"].extend(iroutine["preRoutine"]) break iroutines.append(iroutine) for iroutine in iroutines: routine["interfaceCount"] += 1 decl = { "type": "z_interface%02d" % (routine["interfaceCount"]), "parameters": None, "attributes": [], "vars": [iroutine["name"]], "iroutine": iroutine, "istart": istart, "iend": iend, } routine["parsedDeclarations"].append(decl) elif USE_PARSE_RE.match(jline): routine["use"].append("".join(lines)) else: break routine["declarations"].append("".join(lines)) if ( len(routine["parsedDeclarations"]) == 0 and len(routine["use"]) == 0 and not re.match(" *implicit +none *$", jline, re.IGNORECASE) ): routine["preDeclComments"].append("".join(lines)) else: routine["postDeclComments"].append(comments) while len(lines) > 0: if END_RE.match(jline): routine["end"] = lines break routine["strippedCore"].append(jline) routine["core"].append("".join(lines)) if CONTAINS_RE.match(lines[0]): break m = INCLUDE_RE.match(lines[0]) if m: try: subF = open(m.group("file"), "r", encoding="utf8") subStream = InputStream(subF) while True: subjline, _, sublines = subStream.nextFortranLine() if not sublines: break routine["strippedCore"].append(subjline) subF.close() except Exception: import traceback logger.debug( "error trying to follow include '{}', this might lead to the removal of used variables".format( m.group("file") ) ) if logger.isEnabledFor(logging.DEBUG): traceback.print_exc() jline, _, lines = stream.nextFortranLine() return routine def findWord(word, text, options=re.IGNORECASE): """Returns the position of word in text or -1 if not found. A match is valid only if it is a whole word (i.e. findWord('try','retry') returns false)""" wordRe = re.compile( "(? 100000: raise Error("could not enforce all constraints") m = VAR_RE.match(declarations[idecl2]["vars"][ivar2]) if ( ivar == 0 and findWord(m.group("var").lower(), typeParam) != -1 ): declarations.insert(idecl2 + 1, declarations[idecl]) del declarations[idecl] ivar = 0 moved = 1 break if rest and findWord(m.group("var").lower(), rest) != -1: if len(declarations[idecl]["vars"]) > 1: newDecl = {} newDecl.update(declarations[idecl]) newDecl["vars"] = [declarations[idecl]["vars"][ivar]] declarations.insert(idecl2 + 1, newDecl) del declarations[idecl]["vars"][ivar] else: declarations.insert(idecl2 + 1, declarations[idecl]) del declarations[idecl] ivar = 0 moved = 1 break if moved: break if not moved: ivar += 1 idecl += 1 for i in range(len(declarations) - 1, 0, -1): if ( declarations[i]["normalizedType"].lower() == declarations[i - 1]["normalizedType"].lower() ): declarations[i - 1]["vars"].extend(declarations[i]["vars"]) del declarations[i] def sortDeclarations(declarations): """sorts, compacts declarations and respects dependencies normalizedType has to be defined for the declarations""" declarations.sort(key=lambda x: x["normalizedType"].lower()) for i in range(len(declarations) - 1, 0, -1): if ( declarations[i]["normalizedType"].lower() == declarations[i - 1]["normalizedType"].lower() ): declarations[i - 1]["vars"].extend(declarations[i]["vars"]) del declarations[i] for decl in declarations: decl["vars"].sort(key=lambda x: x.lower()) enforceDeclDependecies(declarations) def writeRoutine(routine, outFile): """writes the given routine to outFile""" outFile.writelines(routine["preRoutine"]) outFile.writelines(routine["begin"]) outFile.writelines(routine["declarations"]) outFile.writelines(routine["core"]) outFile.writelines(routine["end"]) outFile.writelines(routine["postRoutine"]) def writeInCols(dLine, indentCol, maxCol, indentAtt, file): """writes out the strings (trying not to cut them) in dLine up to maxCol indenting each newline with indentCol. The '&' of the continuation line is at maxCol. indentAtt is the actual intent, and the new indent is returned""" maxSize = maxCol - indentCol - 1 tol = min(maxSize / 6, 6) + indentCol for fragment in dLine: if indentAtt + len(fragment) < maxCol: file.write(fragment) indentAtt += len(fragment) elif len(fragment.lstrip()) <= maxSize: file.write("&\n" + (" " * indentCol)) file.write(fragment.lstrip()) indentAtt = indentCol + len(fragment.lstrip()) else: sPieces = STR_RE.split(fragment) for sPiece in sPieces: if sPiece and (not (sPiece[0] == '"' or sPiece[0] == "'")): subPieces = NON_WORD_RE.split(sPiece) else: subPieces = [sPiece] for subPiece in subPieces: if indentAtt == indentCol: file.write(subPiece.lstrip()) indentAtt += len(subPiece.lstrip()) elif indentAtt < tol or indentAtt + len(subPiece) < maxCol: file.write(subPiece) indentAtt += len(subPiece) else: file.write("&\n" + (" " * indentCol)) file.write(subPiece.lstrip()) indentAtt = indentCol + len(subPiece.lstrip()) return indentAtt def writeCompactDeclaration(declaration, file): """Writes a declaration in a compact way""" d = declaration if "iroutine" in d.keys(): file.writelines(d["istart"]) writeRoutine(d["iroutine"], file) file.writelines(d["iend"]) else: if len(d["vars"]) > 0: decl = " " * INDENT_SIZE * 2 + d["type"] if d["parameters"]: # do not drop empty parameter lists? decl += d["parameters"] if d["attributes"]: for a in d["attributes"]: decl += ", " + a decl += " :: " dLine = [decl] for var in d["vars"]: cur_len = sum([len(l) for l in dLine]) if len(dLine) > 1 and cur_len + len(var) > 600: writeInCols(dLine, 3 * INDENT_SIZE, DECL_LINELENGTH, 0, file) file.write("\n") dLine = [decl] if len(dLine) > 1: dLine[-1] += ", " dLine.append(var) writeInCols(dLine, 3 * INDENT_SIZE, DECL_LINELENGTH, 0, file) file.write("\n") def writeExtendedDeclaration(declaration, file): """Writes a declaration in a nicer way (using more space)""" d = declaration if len(d["vars"]) == 0: return if "iroutine" in d.keys(): file.writelines(d["istart"]) writeRoutine(d["iroutine"], file) file.writelines(d["iend"]) else: dLine = [] dLine.append(" " * INDENT_SIZE * 2 + d["type"]) if d["parameters"]: # do not drop empty parameter lists? dLine.append(d["parameters"]) if d["attributes"]: for a in d["attributes"]: dLine[-1:] = [dLine[-1] + ", "] dLine.append(a) indentAtt = writeInCols( dLine, 3 * INDENT_SIZE, DECL_OFFSET + 1 + 2 * INDENT_SIZE, 0, file ) file.write(" " * (DECL_OFFSET + 2 * INDENT_SIZE - indentAtt)) file.write(" :: ") indentAtt = DECL_OFFSET + 8 dLine = [] for var in d["vars"][:-1]: dLine.append(var + ", ") dLine.append(d["vars"][-1]) writeInCols( dLine, DECL_OFFSET + 4 + 2 * INDENT_SIZE, DECL_LINELENGTH, indentAtt, file ) file.write("\n") def writeDeclarations(parsedDeclarations, file): """Writes the declarations to the given file""" for d in parsedDeclarations: maxLenVar = 0 totalLen = 0 for v in d["vars"]: maxLenVar = max(maxLenVar, len(v)) totalLen += len(v) if maxLenVar > 30 or totalLen > DECL_LINELENGTH - 4: writeCompactDeclaration(d, file) else: writeExtendedDeclaration(d, file) def cleanDeclarations(routine, logger): """cleans up the declaration part of the given parsed routine removes unused variables""" if routine["core"]: if CONTAINS_RE.match(routine["core"][-1]): logger.debug( "routine %s contains other routines, declarations not cleaned" % (routine["name"]) ) return nullifyRe = re.compile( r" *nullify *\(([^()]+)\) *\n?", re.IGNORECASE | re.MULTILINE ) if not routine["kind"]: return if routine["core"]: if re.match(" *type *[a-zA-Z_]+ *$", routine["core"][0], re.IGNORECASE): logger.debug( "routine %s contains local types, not fully cleaned" % (routine["name"]) ) if re.match(" *import+ *$", routine["core"][0], re.IGNORECASE): logger.debug( "routine %s contains import, not fully cleaned" % (routine["name"]) ) if re.search("^#", "".join(routine["declarations"]), re.MULTILINE): logger.debug( "routine %s declarations contain preprocessor directives, declarations not cleaned" % (routine["name"]) ) return try: rest = "".join(routine["strippedCore"]).lower() nullifys = ",".join(nullifyRe.findall(rest)) rest = nullifyRe.sub("", rest) paramDecl = [] decls = [] for d in routine["parsedDeclarations"]: d["normalizedType"] = d["type"] if d["parameters"]: d["normalizedType"] += d["parameters"] if d["attributes"]: d["attributes"].sort(key=lambda x: x.lower()) d["normalizedType"] += ", " d["normalizedType"] += ", ".join(d["attributes"]) if any(a.lower() == "parameter" for a in d["attributes"]): paramDecl.append(d) else: decls.append(d) sortDeclarations(paramDecl) sortDeclarations(decls) has_routinen = 0 pos_routinep = -1 for d in paramDecl: for i in range(len(d["vars"])): v = d["vars"][i] m = VAR_RE.match(v) lowerV = m.group("var").lower() if lowerV == "routinen": has_routinen = 1 d["vars"][i] = "routineN = '" + routine["name"] + "'" elif lowerV == "routinep": pos_routinep = i d["vars"][i] = "routineP = moduleN//':'//routineN" if not has_routinen and pos_routinep >= 0: d["vars"].insert(pos_routinep, "routineN = '" + routine["name"] + "'") if routine["arguments"]: routine["lowercaseArguments"] = list( x.lower() for x in routine["arguments"] ) else: routine["lowercaseArguments"] = [] if routine["result"]: routine["lowercaseArguments"].append(routine["result"].lower()) argDeclDict = {} localDecl = [] for d in decls: localD = {} localD.update(d) localD["vars"] = [] argD = None for v in d["vars"]: m = VAR_RE.match(v) lowerV = m.group("var").lower() if lowerV in routine["lowercaseArguments"]: argD = {} argD.update(d) argD["vars"] = [v] if lowerV in argDeclDict.keys(): raise SyntaxError( "multiple declarations not supported. var=" + v + " declaration=" + str(d) + "routine=" + routine["name"] ) argDeclDict[lowerV] = argD else: pos = findWord(lowerV, rest) if pos != -1: localD["vars"].append(v) else: if findWord(lowerV, nullifys) != -1: if not rmNullify(lowerV, routine["core"]): raise SyntaxError( "could not remove nullify of " + lowerV + " as expected, routine=" + routine["name"] ) logger.info( "removed var %s in routine %s\n" % (lowerV, routine["name"]) ) if len(localD["vars"]): localDecl.append(localD) argDecl = [] for arg in routine["lowercaseArguments"]: if arg in argDeclDict.keys(): argDecl.append(argDeclDict[arg]) else: logger.debug( "warning, implicitly typed argument '{}' in routine '{}'".format( arg, routine["name"] ) ) if routine["kind"].lower() == "function": aDecl = argDecl[:-1] else: aDecl = argDecl # try to have arg/param/local, but checks for dependencies arg/param # and param/local argDecl.extend(paramDecl) enforceDeclDependecies(argDecl) splitPos = 0 for i in range(len(argDecl) - 1, -1, -1): if not any(a.lower() == "parameter" for a in argDecl[i]["attributes"]): splitPos = i + 1 break paramDecl = argDecl[splitPos:] argDecl = argDecl[:splitPos] paramDecl.extend(localDecl) enforceDeclDependecies(paramDecl) splitPos = 0 for i in range(len(paramDecl) - 1, -1, -1): if any(a.lower() == "parameter" for a in paramDecl[i]["attributes"]): splitPos = i + 1 break localDecl = paramDecl[splitPos:] paramDecl = paramDecl[:splitPos] newDecl = StringIO() for comment in routine["preDeclComments"]: newDecl.write(comment) newDecl.writelines(routine["use"]) writeDeclarations(argDecl, newDecl) if argDecl and paramDecl: newDecl.write("\n") writeDeclarations(paramDecl, newDecl) if (argDecl or paramDecl) and localDecl: newDecl.write("\n") writeDeclarations(localDecl, newDecl) if argDecl or paramDecl or localDecl: newDecl.write("\n") wrote = 0 for comment in routine["declComments"]: if comment.strip(): newDecl.write(comment.strip()) newDecl.write("\n") wrote = 1 if wrote: newDecl.write("\n") routine["declarations"] = [newDecl.getvalue()] except Exception: if "name" in routine.keys(): logger.critical("exception cleaning routine " + routine["name"]) logger.critical("parsedDeclartions={}".format(routine["parsedDeclarations"])) raise newDecl = StringIO() if routine["postDeclComments"]: comment_start = 0 for comment in routine["postDeclComments"]: if comment.strip(): break else: comment_start += 1 for comment in routine["postDeclComments"][comment_start:]: newDecl.write(comment) newDecl.write("\n") routine["declarations"][0] += newDecl.getvalue() def rmNullify(var, strings): removed = 0 var = var.lower() nullifyRe = re.compile(r" *nullify *\(", re.IGNORECASE) nullify2Re = re.compile( r"(?P *nullify *\()(?P[^()!&]+)\)", re.IGNORECASE ) for i in range(len(strings) - 1, -1, -1): line = strings[i] comments = [] if nullifyRe.match(line) and findWord(var, line) != -1: core = "" comments = [] for l in line.splitlines(): pos = l.find("&") pos2 = l.find("!") if pos == -1: if pos2 == -1: core += l else: core += l[:pos2] comments.append(l[pos2:] + "\n") else: core += l[:pos] if pos2 != -1: comments.append(l[pos2:] + "\n") m = nullify2Re.match(core) if not m: raise SyntaxError( "could not match nullify to " + repr(core) + "in" + repr(line) ) allVars = [] vars = m.group("vars") v = list(s.strip() for s in vars.split(",")) removedNow = 0 for j in range(len(v) - 1, -1, -1): if findWord(var, v[j].lower()) != -1: del v[j] removedNow = 1 if removedNow: if len(v) == 0: if not comments: del strings[i] else: strings[i] = "".join(comments) else: for j in range(len(v) - 1): v[j] += ", " v[-1] += ")" newS = StringIO() v.insert(0, m.group("nullif")) writeInCols(v, len(v[0]) - len(v[0].lstrip()) + 5, 77, 0, newS) newS.write("\n") if comments: for c in comments: newS.write(c) strings[i] = newS.getvalue() removed += 1 return removed def parseUse(inFile): """Parses the use statements in inFile The parsing stops at the first non use statement. Returns something like: ([{'module':'module1','only':['el1','el2=>el3']},...], '! comment1\\n!comment2...\\n', 'last line (the line that stopped the parsing)') """ lineNr = 0 preComments = [] modules = [] origLines = [] commonUses = "" stream = InputStream(inFile) while True: jline, comment_list, lines = stream.nextFortranLine() comments = "\n".join(_ for _ in comment_list if _) lineNr = lineNr + len(lines) if not lines: break origLines.append("".join(lines)) # parse use m = USE_PARSE_RE.match(jline) if m: useAtt = {"module": m.group("module"), "comments": []} if m.group("only"): useAtt["only"] = list(s.strip() for s in m.group("imports").split(",")) else: useAtt["renames"] = list( s.strip() for s in m.group("imports").split(",") ) if useAtt["renames"] == [""]: del useAtt["renames"] if comments: useAtt["comments"].append(comments) # add use to modules modules.append(useAtt) elif jline and not jline.isspace(): break else: if comments and COMMON_USES_RE.match(comments): commonUses += "".join(lines) elif len(modules) == 0: preComments.append(("".join(lines))) elif comments: modules[-1]["comments"].append(comments) return { "modules": modules, "preComments": preComments, "commonUses": commonUses, "postLine": "".join(lines), "origLines": origLines[:-1], } def normalizeModules(modules): """Sorts the modules and their export and removes duplicates. renames aren't sorted correctly""" # orders modules modules.sort(key=lambda x: x["module"]) for i in range(len(modules) - 1, 0, -1): if modules[i]["module"].lower() == modules[i - 1]["module"].lower(): if not ("only" in modules[i - 1].keys() and "only" in modules[i].keys()): raise SyntaxError( "rejoining of module " + str(modules[i]["module"]) + " failed as at least one of the use is not a use ...,only:" ) modules[i - 1]["only"].extend(modules[i]["only"]) del modules[i] # orders imports for m in modules: if "only" in m.keys(): m["only"].sort() for i in range(len(m["only"]) - 1, 0, -1): if m["only"][i - 1].lower() == m["only"][i].lower(): del m["only"][i] def writeUses(modules, outFile): """Writes the use declaration using a long or short form depending on how many only statements there are""" for m in modules: if "only" in m.keys() and len(m["only"]) > 8: writeUseShort(m, outFile) else: writeUseLong(m, outFile) def writeUseLong(m, outFile): """Writes a use declaration in a nicer, but longer way""" if "only" in m.keys(): outFile.write( INDENT_SIZE * " " + "USE " + m["module"] + "," + "ONLY: ".rjust(38 - len(m["module"])) ) if m["only"]: outFile.write(m["only"][0]) for i in range(1, len(m["only"])): outFile.write(",&\n" + "".ljust(43 + INDENT_SIZE) + m["only"][i]) else: outFile.write(INDENT_SIZE * " " + "USE " + m["module"]) if "renames" in m.keys() and m["renames"]: outFile.write("," + "".ljust(38) + m["renames"][0]) for i in range(1, len(m["renames"])): outFile.write(",&\n" + "".ljust(43 + INDENT_SIZE) + m["renames"][i]) if m["comments"]: outFile.write("\n") outFile.write("\n".join(m["comments"])) outFile.write("\n") def writeUseShort(m, file): """Writes a use declaration in a compact way""" uLine = [] if "only" in m.keys(): file.write( INDENT_SIZE * " " + "USE " + m["module"] + "," + "ONLY: &\n".rjust(40 - len(m["module"])) ) for k in m["only"][:-1]: uLine.append(k + ", ") uLine.append(m["only"][-1]) uLine[0] = " " * (5 + INDENT_SIZE) + uLine[0] elif "renames" in m.keys() and m["renames"]: uLine.append(INDENT_SIZE * " " + "USE " + m["module"] + ", ") for k in m["renames"][:-1]: uLine.append(k + ", ") uLine.append(m["renames"][-1]) else: uLine.append(INDENT_SIZE * " " + "USE " + m["module"]) writeInCols(uLine, 5 + INDENT_SIZE, DECL_LINELENGTH, 0, file) if m["comments"]: file.write("\n") file.write("\n".join(m["comments"])) file.write("\n") def prepareImplicitUses(modules): """Transforms a modulesDict into an implictUses (dictionary of module names each containing a dictionary with the only, and the special key '_WHOLE_' which is true if the whole module is implicitly present""" mods = {} for m in modules: m_name = m["module"].lower() if m_name not in mods.keys(): mods[m["module"]] = {"_WHOLE_": 0} m_att = mods[m_name] if "only" in m.keys(): for k in m["only"]: m = LOCAL_NAME_RE.match(k) if not m: raise SyntaxError("could not parse use only:" + repr(k)) impAtt = m.group("localName").lower() m_att[impAtt] = 1 else: m_att["_WHOLE_"] = 1 return mods def cleanUse(modulesDict, rest, implicitUses, logger): """Removes the unneeded modules (the ones that are not used in rest)""" exceptions = {} modules = modulesDict["modules"] rest = rest.lower() for i in range(len(modules) - 1, -1, -1): m_att = {} m_name = modules[i]["module"].lower() if implicitUses and m_name in implicitUses.keys(): m_att = implicitUses[m_name] if "_WHOLE_" in m_att.keys() and m_att["_WHOLE_"]: logger.info("removed USE of module " + m_name) del modules[i] elif "only" in modules[i].keys(): els = modules[i]["only"] for j in range(len(els) - 1, -1, -1): m = LOCAL_NAME_RE.match(els[j]) if not m: raise SyntaxError("could not parse use only:" + repr(els[j])) impAtt = m.group("localName").lower() if impAtt in m_att.keys(): logger.info("removed USE " + m_name + ", only: " + repr(els[j])) del els[j] elif impAtt not in exceptions.keys(): if findWord(impAtt, rest) == -1: logger.info("removed USE " + m_name + ", only: " + repr(els[j])) del els[j] if len(modules[i]["only"]) == 0: if modules[i]["comments"]: modulesDict["preComments"] += [ x + "\n" for x in modules[i]["comments"] ] del modules[i] def resetModuleN(moduleName, lines): "resets the moduleN variable to the module name in the lines lines" for i in range(len(lines)): lines[i] = MODULE_N_RE.sub( " " * INDENT_SIZE + "CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = '" + moduleName + "'", lines[i], ) def rewriteFortranFile( inFile, outFile, indent, decl_linelength, decl_offset, orig_filename=None ): """rewrites the use statements and declarations of inFile to outFile. It sorts them and removes the repetitions.""" import os.path global INDENT_SIZE global DECL_OFFSET global DECL_LINELENGTH INDENT_SIZE = indent DECL_OFFSET = decl_offset DECL_LINELENGTH = decl_linelength coreLines = [] while True: line = inFile.readline() if not line: break if line[0] == "#": coreLines.append(line) outFile.write(line) m = MODULE_RE.match(line) if m: if not orig_filename: orig_filename = inFile.name fn = os.path.basename(orig_filename).rsplit(".", 1)[0] break logger = logging.LoggerAdapter( logging.getLogger("prettify-logger"), {"ffilename": orig_filename} ) modulesDict = parseUse(inFile) routines = [] coreLines.append(modulesDict["postLine"]) routine = parseRoutine(inFile, logger) coreLines.extend(routine["preRoutine"]) if m: resetModuleN(m.group("moduleName"), routine["preRoutine"]) routines.append(routine) while routine["kind"]: routine = parseRoutine(inFile, logger) routines.append(routine) for routine in routines: cleanDeclarations(routine, logger) # in-place modification of 'routine' coreLines.extend(routine["declarations"]) coreLines.extend(routine["strippedCore"]) rest = "".join(coreLines) nonStPrep = 0 for line in modulesDict["origLines"]: if re.search("^#", line) and not COMMON_USES_RE.match(line): logger.debug("noMatch " + repr(line) + "\n") # what does it mean? nonStPrep = 1 if nonStPrep: logger.debug("use statements contains preprocessor directives, not cleaning") outFile.writelines(modulesDict["origLines"]) else: implicitUses = None if modulesDict["commonUses"]: inc_fn = COMMON_USES_RE.match(modulesDict["commonUses"]).group(1) inc_absfn = os.path.join(os.path.dirname(orig_filename), inc_fn) try: with open(inc_absfn, "r", encoding="utf8") as fhandle: implicitUsesRaw = parseUse(fhandle) implicitUses = prepareImplicitUses(implicitUsesRaw["modules"]) except Exception: logger.critical( "failed to parse use statements contained in common uses precompiler file {}".format( inc_absfn ) ) raise cleanUse(modulesDict, rest, implicitUses, logger) normalizeModules(modulesDict["modules"]) outFile.writelines(modulesDict["preComments"]) writeUses(modulesDict["modules"], outFile) outFile.write(modulesDict["commonUses"]) if modulesDict["modules"]: outFile.write("\n") outFile.write(modulesDict["postLine"]) for routine in routines: writeRoutine(routine, outFile)