diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4747b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/README.md b/README.md index 8422f0d..a1fbe3b 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,65 @@ -fortran-legacy-tools +# fortran-legacy-tools -Tools to deal with Fortran code +**Tools to deal with Fortran code** -------------------------------------------------------------------------------- -fixed2free/fixed2free2.py: -------------------------------------------------------------------------------- +**Author:** Elias Rabel -Tool to convert from FORTRAN fixed source form files to free source form. -Supports OpenMP and C-preprocessor statements. +This project includes a suite of tools aimed at facilitating the modernization of Fortran codebases, making them more compatible with contemporary coding standards and practices. -The FORTRAN fixed source format dates back to time when punched cards were -used in programming. Nevertheless it is widespread in the numerical computing -community. Even programs written according to the most recent Fortran 2008 -standard can be written in fixed source form, although this is deprecated since -Fortran 2003. +## fixed2free2.py -This script converts fixed source form files to the free source form, -introduced with Fortran 90. -In refactoring legacy Fortran codes this is a useful first step. +### Description -Some similar tools that I tried, attempt to automatically upgrade -deprecated language constructs with varying success. -This tool takes a more minimalistic approach and changes only the source form. +`fixed2free2.py` converts Fortran code from fixed source form to free source form. This tool is particularly useful in refactoring legacy Fortran codes, enhancing their readability and maintainability. -Automatic unit tests are provided with the Test_fixed2free2.py file. +### Background -------------------------------------------------------------------------------- -flowercase/flowercase.py: -------------------------------------------------------------------------------- +The fixed source format is a legacy from the era of punched cards but remains in use within the numerical computing community. Despite Fortran 2018 allowing for fixed source form, it has been deprecated since Fortran 2003 in favor of the free source form introduced in Fortran 90. -Tool to convert free source form Fortran code to lower-case while -leaving comments and strings unchanged. -Mixed case identifiers and keywords are left unchanged. +### Functionality -------------------------------------------------------------------------------- -fdeclarations/fdeclarations.py: -------------------------------------------------------------------------------- +The tool takes a minimalistic approach by changing only the source form without attempting to modify or upgrade deprecated language constructs. It supports OpenMP and C-preprocessor statements, ensuring a smooth transition to free source form. -Tool to separate subroutine arguments from declarations of local variables. +### Usage -Legacy Fortran subroutines often have huge argument lists. Fortran allows -mixing of argument datatype declarations and declarations of local variables, -which can lead to confusion. +```bash +python fixed2free2.py file.f > file.f90 +``` -This tool generates code for a wrapper of the given subroutine, which -groups declarations into 3 sections: --) parameters (might be needed for dimensions of array arguments) --) subroutine arguments --) local variables (commented out) +### Limitations +While fixed2free2.py aims to accurately transform fixed form code to free form, it has specific limitations regarding whitespace usage that can affect the conversion: + +The script cannot handle certain usages of whitespace characters allowed in fixed form but not in free form. This limitation is crucial when dealing with complex formatting that does not directly translate into free form. + +For example, the following fixed form source code: + +```Fortran + WR IT E(* , *) I J K LM N +``` + +will not be transformed into correct free form source code, which would be: + +```Fortran +WRITE (*,*) IJKLMN +``` + +[issue2]: https://github.com/ylikx/fortran-legacy-tools/issues/2 + +## Additional Tools in the Project + +### flowercase.py: + +A tool to convert free source form Fortran code to lowercase, excluding comments and strings, while leaving mixed case identifiers unchanged. + +### fdeclarations.py: + +Assists in separating subroutine arguments from local variable declarations, enhancing code clarity and organization. + +Legacy Fortran subroutines often have huge argument lists. Fortran allows mixing of argument datatype declarations and declarations of local variables, which can lead to confusion. + +This tool generates code for a wrapper of the given subroutine, which groups declarations into 3 sections: + +- parameters (might be needed for dimensions of array arguments) +- subroutine arguments +- local variables (commented out) diff --git a/fdeclarations/fdeclarations.py b/fdeclarations/fdeclarations.py deleted file mode 100644 index 41655b7..0000000 --- a/fdeclarations/fdeclarations.py +++ /dev/null @@ -1,380 +0,0 @@ -# -*- coding: utf-8 -*- - -# fdeclarations.py: Tool to separate subroutine arguments from -# declarations of local variables -# -# Copyright (C) 2012 Elias Rabel -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Tool to separate subroutine arguments from declarations of local variables. - -Legacy Fortran subroutines often have huge argument lists. Fortran allows -mixing of argument datatype declarations and declarations of local variables, -which can lead to confusion. - -This tool generates code for a wrapper of the given subroutine, which -separates declarations into 3 sections: --) parameters (might be needed for dimensions of array arguments) --) subroutine arguments --) local variables (commented out) - -Furthermore a conversion to the Fortran 90 declaration style is performed. - -Usage: python fdeclarations.py file.f90 > output.f90 - -Restrictions: - -*) Assumes free source format -*) Assumes 'implicit none' -*) Only one subroutine per file allowed - -*) There might be problems when working with Fortran functions -Workaround: replace 'function' keyword temporarily with 'subroutine' - -*) Warning: does not deal with attributes in different lines: -*) e.g.: -*) double complex :: CONE -*) parameter (CONE = (1.0d0, 0.0d0)) - -*) does not deal with constructs like real * 8 or real (8) -*) in F77 declaration style if not written without spaces - - -author: Elias Rabel -Let me know when you find this script useful: -ylikx.0 at gmail -https://www.github.com/ylikx/ - -""" - -from __future__ import print_function - -import re -import sys - -TYPEKEYS = ['integer', 'logical', 'real', 'complex', - 'double precision', 'double complex', 'character', 'doubleprecision', 'doublecomplex'] - -#m = re.search(r'(subroutine|function)(.*?)\((.*?)\)', s) - - -#class FortranVariableDict - -class FortranVariable: - def __init__(self, name, decl, dim, initialiser, is_argument=False): - self.name = name - self.decl = decl - self.dim = dim - self.initialiser = initialiser - self.is_argument = is_argument - - if 'parameter' in self.decl: - self.is_parameter = True - else: - self.is_parameter = False - - - def __repr__(self): - return str([self.name, self.decl, self.dim, self.initialiser, self.is_argument]) - - def getDeclString(self): - """Returns a F90-declaration string.""" - string = self.decl - if self.dim: - string += ", dimension" + self.dim - string += " :: " + self.name - if self.initialiser: - string += " =" + self.initialiser - return string - -class FortranSubroutineHeader: - def __init__(name, arglist): - self.name = '' - self.arglist = [] - -#------------------------------------------------------------------------------ -def gen_removeComments(stream): - """Remove Fortran comments from stream considering strings""" - - - for line in stream: - string_delim = None - result_line = '' - - for ch in line: - if ch in ["'", '"']: - if not string_delim: - string_delim = ch - elif string_delim == ch: - string_delim = None - - if ch == '!' and not string_delim: - result_line += '\n' - break - - result_line += ch - - if result_line.isspace(): - continue - - yield result_line - - -#------------------------------------------------------------------------------ -def gen_removeLineContinuations(stream): - result_line = '' - - for line in stream: - if re.search(r'&\s*$', line): - result_line = result_line.rstrip() + line.rstrip().rstrip('&').lstrip() - continue - else: - result_line += line - - yield result_line - result_line = '' - -#------------------------------------------------------------------------------ -def gen_removeEmptyLines(stream): - """Creates Generator that returns only non-empty lines of a stream""" - for line in stream: - #skip empty lines - if line.isspace(): - continue - yield line - - -#TODO integer function etc... problematic -# Misinterpretes function declaration -#------------------------------------------------------------------------------ -def isDeclarationLine(line): - - for ty in TYPEKEYS: - # search for whole words only - if re.search(r'\b'+ty+r'\b', line, re.IGNORECASE): - return True - - return False - -#------------------------------------------------------------------------------ -def separate_names_and_dims(varstr): - """Removes dimension list. E.g.: "var (n,m), x(5)" -> ("var, x", "(n,m) (5)") """ - names = [] - dims = [] - - namestr = '' - dimstr = '' - - num_par = 0 - - for ch in varstr: - - if not ch in '()': - if num_par == 0: - namestr += ch - else: - dimstr += ch - elif ch == '(': - num_par += 1 - dimstr += ch - elif ch == ')': - num_par -= 1 - dimstr += ch - - if ch == ',' and num_par == 0: - namestr = namestr.replace(',', '') - # new variable - names.append(namestr.strip()) - dims.append(dimstr.strip()) - namestr = '' - dimstr = '' - - names.append(namestr.strip()) - dims.append(dimstr.strip()) - - return names, dims - -#------------------------------------------------------------------------------ -def removeDimension(x): - """Removes dimension list. E.g.: "var (n,m), x(5)" -> "var, x" """ - result = '' - num_par = 0 - for ch in x: - if not ch in '()': - if num_par == 0: - result += ch - elif ch == '(': - num_par += 1 - elif ch == ')': - num_par -= 1 - return result - -#------------------------------------------------------------------------------ -def getVarsF90Style(line): - ind = line.find('::') - varstr = line[ind+2:].strip() - decl = line[:ind].strip() - return decl, varstr - -#------------------------------------------------------------------------------ -def getVarsF77Style(line): - #([*][0-9]+)? is to support stuff like real*8 - #varstr = re.sub('('+'|'.join(TYPEKEYS) + r')([*][0-9]+)?.*?', '', line, re.IGNORECASE).strip() - # deal also with stuff like real(8) - #print re.match('('+'|'.join(TYPEKEYS) + r')(([*][0-9]+)|\(.+?\))?.*?', line, re.IGNORECASE).group(0) - #varstr = re.sub('('+'|'.join(TYPEKEYS) + r')(([*][0-9]+)|\(.+?\))?', '', line, re.IGNORECASE).strip() - #print varstr - #ind = line.find(varstr) - #decl = line[:ind].strip() - sline = line.split() - decl = sline.pop(0).strip() - if decl.lower() == 'double': - decl += ' ' + sline.pop(0).strip() - varstr = ' '.join(sline) - - return decl, varstr - -#------------------------------------------------------------------------------ -def extractInitialiser(expr): - split_expr = expr.split('=') - if len(split_expr) > 1: - return split_expr[0].strip(), split_expr[1].strip() - else: - return split_expr[0].strip(), None - -#------------------------------------------------------------------------------ -def getVariablenames(line): - """Given a declaration line, returns a list of names of - variables declared in this line and the declaration part""" - - if '::' in line: - decl, varstr = getVarsF90Style(line) - else: - decl, varstr = getVarsF77Style(line) - - # if initialisation of varible in same line - extract it - varstr, initialiser = extractInitialiser(varstr) - - #names = [name.strip() for name in removeDimension(varstr).split(',')] - - names, dims = separate_names_and_dims(varstr) - return decl, names, dims, initialiser - - -#----------------------------------------------------------------------------- -def getArgumentList(argline): - """Returns the argument list of a Fortran subroutine""" - #print line - re_match = re.search(r'subroutine(.*?)\((.*)\)', argline, re.IGNORECASE) - - if re_match == None: - return None - - #print re_match.group(0) - - name = re_match.group(1).strip() - arglist = [arg.strip() for arg in re_match.group(2).split(',')] - - return name, arglist - -#TODO: store declaration line, extract type info, save dimension info - -#------------------------------------------------------------------------------ -def printWrapperCode(subname, arglist, varlist): - print("!" + "-"*79) - print("!> A wrapper for the subroutine " + subname) - #for entry in arglist: - # print "!> @param " + entry - print("subroutine " + subname + "_wrapper(" + ','.join(arglist) + ')') - print() - print(" implicit none") - print() - # parameters first - print( " ! Parameters") - for entry in varlist: - if not entry.is_argument and entry.is_parameter: - print (" " + entry.getDeclString()) - # print scalar arguments - print() - print( " ! Arguments") - for entry in varlist: - if entry.is_argument: - print (" " + entry.getDeclString()) - # print array arguments - #print - #print " ! Array arguments" - #for entry in varlist: - # if entry.is_argument and entry.dim: - # print " " + entry.getDeclString() - print() - print(" ! Former local variables of " + subname) - for entry in varlist: - if not entry.is_argument: - print(" ! " + entry.getDeclString()) - - print() - print (" call " + subname + "(" + ','.join(arglist) + ')') - print() - print("end subroutine") - -if __name__ == "__main__": - - f = open(sys.argv[1], 'r') - - xf = gen_removeEmptyLines(gen_removeLineContinuations(gen_removeComments(f))) - - class NoArgList: - pass - - # get argument list first: - args = None - - for line in xf: - temp = getArgumentList(line) - if temp != None: - subname, args = temp - break - - if not args: - print("ERROR: no subroutine header found!") - raise NoArgList - - vardict = {} - varlist = [] - - for line in xf: - if isDeclarationLine(line): - decl, names, dims, initstr = getVariablenames(line) - #print getVariablenames(line) - for name, dim in zip(names, dims): - entry = FortranVariable(name, decl, dim, initstr, is_argument=False) - vardict[name.lower()] = entry - varlist.append(entry) - - if "end subroutine" in line: - break - - #print varlist - - # Flag arguments - for arg in args: - vardict[arg.lower()].is_argument = True - - - f.close() - - printWrapperCode(subname, args, varlist) - diff --git a/fixed2free/README.md b/fixed2free/README.md deleted file mode 100644 index 72a0147..0000000 --- a/fixed2free/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# fixed2free2.py -Author: Elias Rabel - -Tool to convert from FORTRAN fixed source form files to free source form. -Supports OpenMP and C-preprocessor statements. - -The FORTRAN fixed source format dates back to time when punched cards were -used in programming. Nevertheless it is widespread in the numerical computing -community. Even programs written according to the most recent Fortran 2018 -standard can be written in fixed source form, although this is deprecated since -Fortran 2003. - -This script converts fixed source form files to the free source form, -introduced with Fortran 90. -In refactoring legacy Fortran codes this is a useful first step. - -Some similar tools that I tried, attempt to automatically upgrade -deprecated language constructs with varying success. -This tool takes a more minimalistic approach and changes only the source form. - -Usage: - -```bash -python fixed2free2.py file.f > file.f90 -``` - -## Limitations - -This script can not handle certain usage of whitespace characters that is allowed in fixed -form but not in free form source code (see [#2][issue2]). - -For example: - -The following fixed form source code - -```Fortran - WR IT E(* , *) I J K LM N -``` - -will not be transformed into correct free form source code, which would be: - -```Fortran -WRITE (*,*) IJKLMN -``` - -[issue2]: https://github.com/ylikx/fortran-legacy-tools/issues/2 diff --git a/fixed2free/setup.py b/fixed2free/setup.py deleted file mode 100755 index 858449d..0000000 --- a/fixed2free/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from distutils.core import setup - -mymail = '12345.0$gmail.com'.replace('1','y').replace('2','l') -mymail = mymail.replace('3','i').replace('4','k').replace('5','x').replace('$', '@') - -setup(name='fixed2free2', - version='0.8', - description='Fortran fixed to free source form converter', - long_description="""Tool to convert from FORTRAN fixed source form files to free source form. -Supports OpenMP and C-preprocessor statements. - -The FORTRAN fixed source format dates back to time when punched cards were -used in programming. Nevertheless it is widespread in the numerical computing -community. Even programs written according to the most recent Fortran 2008 -standard can be written in fixed source form, although this is deprecated since -Fortran 2003. - -This script converts fixed source form files to the free source form, -introduced with Fortran 90. -In refactoring legacy Fortran codes this is a useful first step. - -Some similar tools that I tried, attempt to automatically upgrade -deprecated language constructs with varying success. -This tool takes a more minimalistic approach and changes only the source form. -""", - author='Elias Rabel', - author_email=mymail, - url='https://github.com/ylikx/fortran-legacy-tools', - scripts=['fixed2free2.py'], - classifiers=['Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Fortran', - 'Topic :: Software Development', - 'Topic :: Software Development :: Code Generators', - 'Topic :: Scientific/Engineering',] - ) - -#TODO: download_url diff --git a/flowercase/flowercase.py b/flowercase/flowercase.py deleted file mode 100644 index d97c3ab..0000000 --- a/flowercase/flowercase.py +++ /dev/null @@ -1,77 +0,0 @@ -# flowercase.py: Conversion of Fortran code from traditional all -# uppercase source to more readable lowercase. -# -# Copyright (C) 2012-2021 Elias Rabel -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Script that converts free form Fortran code to lower case -without messing with comments or strings. Mixed case words remain -untouched. - -Note: works only in free source form, use fixed2free.py first to -convert. - -Usage: file name as first command line parameter -""" - -# author: Elias Rabel, 2012 -# Let me know when you find this script useful: -# ylikx.0 at gmail -# https://www.github.com/ylikx/ - -from __future__ import print_function - -import sys - -infile = open(sys.argv[1], 'r') - -commentmode = False -stringmode = False -stringchar = '' - - -for line in infile: - line_new = '' - word = '' - commentmode = False - - for character in line: - if not character.isalnum() and character != '_': - - if not stringmode and not commentmode: - if word.isupper(): # means: do not convert mixed case words - word = word.lower() - - line_new += word - line_new += character - word = '' - - if (character == '"' or character == "'") and not commentmode: - if not stringmode: - stringchar = character - stringmode = True - else: - stringmode = not (character == stringchar) - - if character == '!' and not stringmode: - commentmode = True # treat rest of line as comment - - else: - word += character - - print(line_new, end="") - -infile.close() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f16fe39 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "fortran-legacy-tools" +dynamic = ["version"] +description = "Tools to deal with Fortran code" +dependencies = ["docopt-ng"] + +[[project.authors]] +name = "Elias Rabel" + +[[project.authors]] +name = "Jacob Williams" + +[[project.authors]] +name = "Christopher Saloman" + +[project.optional-dependencies] +development = ["wheel", "pytest"] + +[project.scripts] +fixed2free = "fortran_legacy_tools.fixed2free2:main" +fdeclarations = "fortran_legacy_tools.fdeclarations:main" +flowercase = "fortran_legacy_tools.flowercase:main" + +[tool.setuptools.packages.find] +namespaces = true +where = ["src"] diff --git a/src/fortran_legacy_tools/__init__.py b/src/fortran_legacy_tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fortran_legacy_tools/fdeclarations.py b/src/fortran_legacy_tools/fdeclarations.py new file mode 100644 index 0000000..556abc8 --- /dev/null +++ b/src/fortran_legacy_tools/fdeclarations.py @@ -0,0 +1,392 @@ +# -*- coding: utf-8 -*- + +# fdeclarations.py: Tool to separate subroutine arguments from +# declarations of local variables +# +# Copyright (C) 2012 Elias Rabel +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Separates subroutine arguments from declarations of local variables and +converts to the Fortran 90 declaration style. + +Usage: + fdeclarations.py + +Options: + -h --help Show this screen. + Input Fortran 90 file name containing the subroutine. + +Restrictions: + - Assumes free source format. + - Assumes 'implicit none'. + - Only one subroutine per file allowed. + - There might be problems when working with Fortran functions. Workaround: replace 'function' keyword temporarily with 'subroutine'. + - Warning: does not deal with attributes in different lines (e.g., double complex :: CONE, parameter (CONE = (1.0d0, 0.0d0))). + - Does not deal with constructs like real * 8 or real (8) in F77 declaration style if not written without spaces. + +Description: + This tool generates code for a wrapper of the given subroutine, which + separates declarations into three sections: parameters (for dimensions of array arguments), + subroutine arguments, and local variables (commented out). Additionally, + it converts declarations to the Fortran 90 style. The result is printed to + standard output, which can be redirected to a file. + +Example: + python fdeclarations.py my_subroutine.f90 > updated_subroutine.f90 +""" + +# author: Elias Rabel +# Let me know when you find this script useful: +# ylikx.0 at gmail +# https://www.github.com/ylikx/ + + +from __future__ import print_function + +import re +import sys + +from docopt import docopt + +TYPEKEYS = [ + "integer", + "logical", + "real", + "complex", + "double precision", + "double complex", + "character", + "doubleprecision", + "doublecomplex", +] + +# m = re.search(r'(subroutine|function)(.*?)\((.*?)\)', s) + + +# class FortranVariableDict + + +class FortranVariable: + def __init__(self, name, decl, dim, initialiser, is_argument=False): + self.name = name + self.decl = decl + self.dim = dim + self.initialiser = initialiser + self.is_argument = is_argument + + if "parameter" in self.decl: + self.is_parameter = True + else: + self.is_parameter = False + + def __repr__(self): + return str([self.name, self.decl, self.dim, self.initialiser, self.is_argument]) + + def getDeclString(self): + """Returns a F90-declaration string.""" + string = self.decl + if self.dim: + string += ", dimension" + self.dim + string += " :: " + self.name + if self.initialiser: + string += " =" + self.initialiser + return string + + +class FortranSubroutineHeader: + def __init__(name, arglist): + self.name = "" + self.arglist = [] + + +# ------------------------------------------------------------------------------ +def gen_removeComments(stream): + """Remove Fortran comments from stream considering strings""" + + for line in stream: + string_delim = None + result_line = "" + + for ch in line: + if ch in ["'", '"']: + if not string_delim: + string_delim = ch + elif string_delim == ch: + string_delim = None + + if ch == "!" and not string_delim: + result_line += "\n" + break + + result_line += ch + + if result_line.isspace(): + continue + + yield result_line + + +# ------------------------------------------------------------------------------ +def gen_removeLineContinuations(stream): + result_line = "" + + for line in stream: + if re.search(r"&\s*$", line): + result_line = result_line.rstrip() + line.rstrip().rstrip("&").lstrip() + continue + else: + result_line += line + + yield result_line + result_line = "" + + +# ------------------------------------------------------------------------------ +def gen_removeEmptyLines(stream): + """Creates Generator that returns only non-empty lines of a stream""" + for line in stream: + # skip empty lines + if line.isspace(): + continue + yield line + + +# TODO integer function etc... problematic +# Misinterpretes function declaration +# ------------------------------------------------------------------------------ +def isDeclarationLine(line): + for ty in TYPEKEYS: + # search for whole words only + if re.search(r"\b" + ty + r"\b", line, re.IGNORECASE): + return True + + return False + + +# ------------------------------------------------------------------------------ +def separate_names_and_dims(varstr): + """Removes dimension list. E.g.: "var (n,m), x(5)" -> ("var, x", "(n,m) (5)")""" + names = [] + dims = [] + + namestr = "" + dimstr = "" + + num_par = 0 + + for ch in varstr: + if not ch in "()": + if num_par == 0: + namestr += ch + else: + dimstr += ch + elif ch == "(": + num_par += 1 + dimstr += ch + elif ch == ")": + num_par -= 1 + dimstr += ch + + if ch == "," and num_par == 0: + namestr = namestr.replace(",", "") + # new variable + names.append(namestr.strip()) + dims.append(dimstr.strip()) + namestr = "" + dimstr = "" + + names.append(namestr.strip()) + dims.append(dimstr.strip()) + + return names, dims + + +# ------------------------------------------------------------------------------ +def removeDimension(x): + """Removes dimension list. E.g.: "var (n,m), x(5)" -> "var, x" """ + result = "" + num_par = 0 + for ch in x: + if not ch in "()": + if num_par == 0: + result += ch + elif ch == "(": + num_par += 1 + elif ch == ")": + num_par -= 1 + return result + + +# ------------------------------------------------------------------------------ +def getVarsF90Style(line): + ind = line.find("::") + varstr = line[ind + 2 :].strip() + decl = line[:ind].strip() + return decl, varstr + + +# ------------------------------------------------------------------------------ +def getVarsF77Style(line): + # ([*][0-9]+)? is to support stuff like real*8 + # varstr = re.sub('('+'|'.join(TYPEKEYS) + r')([*][0-9]+)?.*?', '', line, re.IGNORECASE).strip() + # deal also with stuff like real(8) + # print re.match('('+'|'.join(TYPEKEYS) + r')(([*][0-9]+)|\(.+?\))?.*?', line, re.IGNORECASE).group(0) + # varstr = re.sub('('+'|'.join(TYPEKEYS) + r')(([*][0-9]+)|\(.+?\))?', '', line, re.IGNORECASE).strip() + # print varstr + # ind = line.find(varstr) + # decl = line[:ind].strip() + sline = line.split() + decl = sline.pop(0).strip() + if decl.lower() == "double": + decl += " " + sline.pop(0).strip() + varstr = " ".join(sline) + + return decl, varstr + + +# ------------------------------------------------------------------------------ +def extractInitialiser(expr): + split_expr = expr.split("=") + if len(split_expr) > 1: + return split_expr[0].strip(), split_expr[1].strip() + else: + return split_expr[0].strip(), None + + +# ------------------------------------------------------------------------------ +def getVariablenames(line): + """Given a declaration line, returns a list of names of + variables declared in this line and the declaration part""" + + if "::" in line: + decl, varstr = getVarsF90Style(line) + else: + decl, varstr = getVarsF77Style(line) + + # if initialisation of varible in same line - extract it + varstr, initialiser = extractInitialiser(varstr) + + # names = [name.strip() for name in removeDimension(varstr).split(',')] + + names, dims = separate_names_and_dims(varstr) + return decl, names, dims, initialiser + + +# ----------------------------------------------------------------------------- +def getArgumentList(argline): + """Returns the argument list of a Fortran subroutine""" + # print line + re_match = re.search(r"subroutine(.*?)\((.*)\)", argline, re.IGNORECASE) + + if re_match == None: + return None + + # print re_match.group(0) + + name = re_match.group(1).strip() + arglist = [arg.strip() for arg in re_match.group(2).split(",")] + + return name, arglist + + +# TODO: store declaration line, extract type info, save dimension info + + +# ------------------------------------------------------------------------------ +def printWrapperCode(subname, arglist, varlist): + print("!" + "-" * 79) + print("!> A wrapper for the subroutine " + subname) + # for entry in arglist: + # print "!> @param " + entry + print("subroutine " + subname + "_wrapper(" + ",".join(arglist) + ")") + print() + print(" implicit none") + print() + # parameters first + print(" ! Parameters") + for entry in varlist: + if not entry.is_argument and entry.is_parameter: + print(" " + entry.getDeclString()) + # print scalar arguments + print() + print(" ! Arguments") + for entry in varlist: + if entry.is_argument: + print(" " + entry.getDeclString()) + # print array arguments + # print + # print " ! Array arguments" + # for entry in varlist: + # if entry.is_argument and entry.dim: + # print " " + entry.getDeclString() + print() + print(" ! Former local variables of " + subname) + for entry in varlist: + if not entry.is_argument: + print(" ! " + entry.getDeclString()) + + print() + print(" call " + subname + "(" + ",".join(arglist) + ")") + print() + print("end subroutine") + + +def main(): + args = docopt(__doc__) + + with open(args[""], "r") as f: + xf = gen_removeEmptyLines(gen_removeLineContinuations(gen_removeComments(f))) + + # get argument list first: + args = None + + for line in xf: + temp = getArgumentList(line) + if temp is not None: + subname, args = temp + break + + if not args: + raise NoArgList("ERROR: no subroutine header found!") + + vardict = {} + varlist = [] + + for line in xf: + if isDeclarationLine(line): + decl, names, dims, initstr = getVariablenames(line) + # print getVariablenames(line) + for name, dim in zip(names, dims): + entry = FortranVariable(name, decl, dim, initstr, is_argument=False) + vardict[name.lower()] = entry + varlist.append(entry) + + if "end subroutine" in line: + break + + # print varlist + + # Flag arguments + for arg in args: + vardict[arg.lower()].is_argument = True + + printWrapperCode(subname, args, varlist) + + +class NoArgList(Exception): + pass + +if __name__ == "__main__": + main() diff --git a/fixed2free/fixed2free2.py b/src/fortran_legacy_tools/fixed2free2.py old mode 100755 new mode 100644 similarity index 64% rename from fixed2free/fixed2free2.py rename to src/fortran_legacy_tools/fixed2free2.py index efd54f2..0197602 --- a/fixed2free/fixed2free2.py +++ b/src/fortran_legacy_tools/fixed2free2.py @@ -20,23 +20,35 @@ # along with this program. If not, see . """ -Script that converts fixed form Fortran code to free form -Usage: file name as first command line parameter +Converts fixed form Fortran code to free form. -python fixed2free2.py file.f > file.f90 +Usage: + fixed2free2.py + +Options: + -h --help Show this screen. + Input file name with fixed form Fortran code. + +Description: + This script takes a fixed form Fortran source file as input and converts + it to free form Fortran code. The output is printed to standard output, + which can be redirected to a file using '>' in the command line. + +Example: + python fixed2free2.py my_legacy_code.f > my_updated_code.f90 """ # author: Elias Rabel, 2012 # Let me know if you find this script useful: # ylikx.0 at gmail # https://www.github.com/ylikx/ - -# TODO: -# *) Improve command line usage - from __future__ import print_function + import sys +from docopt import docopt + + class FortranLine: def __init__(self, line): self.line = line @@ -44,57 +56,61 @@ def __init__(self, line): self.isComment = False self.isContinuation = False self.__analyse() - + def __repr__(self): return self.line_conv - + def continueLine(self): """Insert line continuation symbol at correct position in a free format line.""" if not self.isOMP: - before_inline_comment, inline_comment = extract_inline_comment(self.line_conv) + before_inline_comment, inline_comment = extract_inline_comment( + self.line_conv + ) else: tmp, inline_comment = extract_inline_comment(self.line_conv[1:].lstrip()) before_inline_comment = "!" + tmp - + if inline_comment == "": self.line_conv = self.line_conv.rstrip() + " &\n" else: len_before = len(before_inline_comment) before = before_inline_comment.rstrip() + " & " self.line_conv = before.ljust(len_before) + inline_comment - + def __analyse(self): line = self.line - firstchar = line[0] if len(line) > 0 else '' - self.label = line[0:5].strip().lower() + ' ' if len(line) > 1 else '' - cont_char = line[5] if len(line) >= 6 else '' - fivechars = line[1:5] if len(line) > 1 else '' - self.isShort = (len(line) <= 6) - self.isLong = (len(line) > 73) - + firstchar = line[0] if len(line) > 0 else "" + self.label = line[0:5].strip().lower() + " " if len(line) > 1 else "" + cont_char = line[5] if len(line) >= 6 else "" + fivechars = line[1:5] if len(line) > 1 else "" + self.isShort = len(line) <= 6 + self.isLong = len(line) > 73 + self.isComment = firstchar in "cC*!" - self.isNewComment = '!' in fivechars and not self.isComment + self.isNewComment = "!" in fivechars and not self.isComment self.isOMP = self.isComment and fivechars.lower() == "$omp" if self.isOMP: self.isComment = False - self.label = '' - self.isCppLine = (firstchar == '#') - self.is_regular = (not (self.isComment or self.isNewComment or - self.isCppLine or self.isShort)) - self.isContinuation = (not (cont_char.isspace() or cont_char == '0') and - self.is_regular) - - self.code = line[6:] if len(line) > 6 else '\n' - - self.excess_line = '' + self.label = "" + self.isCppLine = firstchar == "#" + self.is_regular = not ( + self.isComment or self.isNewComment or self.isCppLine or self.isShort + ) + self.isContinuation = ( + not (cont_char.isspace() or cont_char == "0") and self.is_regular + ) + + self.code = line[6:] if len(line) > 6 else "\n" + + self.excess_line = "" if self.isLong and self.is_regular: code, inline_comment = extract_inline_comment(self.code) if inline_comment == "" or len(code) >= 72 - 6: self.excess_line = line[72:] - line = line[:72] + '\n' + line = line[:72] + "\n" self.code = line[6:] - + self.line = line self.__convert() @@ -102,69 +118,72 @@ def __convert(self): line = self.line if self.isComment: - self.line_conv = '!' + line[1:] + self.line_conv = "!" + line[1:] elif self.isNewComment or self.isCppLine: self.line_conv = line elif self.isOMP: - self.line_conv = '!' + line[1:5] + ' ' + self.code + self.line_conv = "!" + line[1:5] + " " + self.code elif not self.label.isspace(): self.line_conv = self.label + self.code else: self.line_conv = self.code - if self.excess_line != '': + if self.excess_line != "": if self.excess_line.lstrip().startswith("!"): marker = "" else: marker = "!" - - self.line_conv = self.line_conv.rstrip().ljust(72) + marker + self.excess_line + + self.line_conv = ( + self.line_conv.rstrip().ljust(72) + marker + self.excess_line + ) + def extract_inline_comment(code): """Splits line of code into (code, inline comment)""" stringmode = False stringchar = "" - + for column, character in enumerate(code): - is_string_delimiter = (character == "'" or character == '"') + is_string_delimiter = character == "'" or character == '"' if not stringmode and is_string_delimiter: stringmode = True stringchar = character elif stringmode and is_string_delimiter: - stringmode = (character != stringchar) + stringmode = character != stringchar elif not stringmode and character == "!": return code[:column], code[column:] - + return code, "" def convertToFree(stream): """Convert stream from fixed source form to free source form.""" linestack = [] - + for line in stream: convline = FortranLine(line) - + if convline.is_regular: - if convline.isContinuation and linestack: + if convline.isContinuation and linestack: linestack[0].continueLine() - for l in linestack: - yield str(l) + for line in linestack: + yield str(line) linestack = [] - + linestack.append(convline) - - for l in linestack: - yield str(l) - -if __name__ == "__main__": + for line in linestack: + yield str(line) + + +def main(): + args = docopt(__doc__) - if len(sys.argv) > 1: - infile = open(sys.argv[1], 'r') - for line in convertToFree(infile): + with open(args[""], "r") as f: + for line in convertToFree(f): print(line, end="") - - infile.close() - else: - print(__doc__) + + +if __name__ == "__main__": + main() diff --git a/src/fortran_legacy_tools/flowercase.py b/src/fortran_legacy_tools/flowercase.py new file mode 100644 index 0000000..e50e91f --- /dev/null +++ b/src/fortran_legacy_tools/flowercase.py @@ -0,0 +1,90 @@ +# flowercase.py: Conversion of Fortran code from traditional all +# uppercase source to more readable lowercase. +# +# Copyright (C) 2012-2021 Elias Rabel +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Converts free form Fortran code to lower case without altering comments, strings, +or mixed case words. + +Usage: + flowercase + +Options: + -h --help Show this screen. + Input file name with free form Fortran code. + +Notes: + - This script is designed to work only with free source form. Use fixed2free.py first if you need to convert from fixed to free source form. + - The script lowers the case of all Fortran keywords and variables that are entirely in uppercase, while preserving the case of mixed case identifiers, strings, and comments. + +Example: + flowercase my_program.f90 > updated_program.f90 +""" + +# author: Elias Rabel, 2012 +# Let me know when you find this script useful: +# ylikx.0 at gmail +# https://www.github.com/ylikx/ + +from __future__ import print_function + +import sys + +from docopt import docopt + + +def main(): + args = docopt(__doc__) + infile = open(sys.argv[1], "r") + + commentmode = False + stringmode = False + stringchar = "" + + with open(args[""], "r") as infile: + for line in infile: + line_new = "" + word = "" + commentmode = False + + for character in line: + if not character.isalnum() and character != "_": + if not stringmode and not commentmode: + if word.isupper(): # means: do not convert mixed case words + word = word.lower() + + line_new += word + line_new += character + word = "" + + if (character == '"' or character == "'") and not commentmode: + if not stringmode: + stringchar = character + stringmode = True + else: + stringmode = not (character == stringchar) + + if character == "!" and not stringmode: + commentmode = True # treat rest of line as comment + + else: + word += character + + print(line_new, end="") + + +if __name__ == "__main__": + main() diff --git a/fixed2free/Test_fixed2free2.py b/test/test_fixed2free2.py similarity index 78% rename from fixed2free/Test_fixed2free2.py rename to test/test_fixed2free2.py index 455dca8..582def5 100755 --- a/fixed2free/Test_fixed2free2.py +++ b/test/test_fixed2free2.py @@ -1,45 +1,43 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import unittest -from fixed2free2 import * +import pytest +from fortran_legacy_tools.fixed2free2 import * + try: from StringIO import StringIO except ImportError: from io import StringIO teststr = [ -""" + """ C This is a Fortran Comment * this also. """, - -""" + """ A = B * C + D + +EF**2 """, - -""" + """ WRITE(*,*) "Just a regular F77 line." """, - -""" + """ C$OMP PARALLEL DO PRIVATE(X1, X2, C$OMP& X3, X4) C some code... C$OMP END PARALLEL DO C$OMP """, -""" + """ #define NDEBUG """, -""" + """ CALL FOO(A, B, C, C a comment * another comment +D, E, F) """, -""" + """ ! Comment1 ! Comment2 ! Comment3 @@ -49,23 +47,23 @@ !C + D 0WRITE(*,*) "Regular line." """, -""" + """ c """, -""" + """ Csho """, -""" + """ C An empty line and an ! continuation character X = SIN(A) * COS(B) + & SIN(B) * COS(A) + ! X """, -""" + """ 10 CONTINUE """, -""" + """ C Test with code that uses text after 72nd column as comments ENDOFLINE E = M * C**2 COMMENT CALL FUNC(A, B, C, COMMENT @@ -73,80 +71,71 @@ C comment inbetween WHY $ G, H, I) SOMETHING """, - -""" + """ 1000 E = ! an inline comment in a continued line + 2 * 3 """, - -# do not rip apart inline comments -""" + # do not rip apart inline comments + """ E = 42 ! inline comment extending beyond column 72 """, - -""" + """ E = 42 ! just a short inline comment """, - -""" + """ C = "!" // & '!' // ABC & "!'!" // & "A" """, -# exclamation mark after column 72 -""" + # exclamation mark after column 72 + """ E = LI ABC!DEF """, -# line with exactly 72 cols + newline -""" + # line with exactly 72 cols + newline + """ E = 72 """, - -# lines with ! at col 72/73 -""" + # lines with ! at col 72/73 + """ E = LI !Comment E = 72 !Comment """, - -# inline comment after col 72 -""" + # inline comment after col 72 + """ E = LI !Comment -""" +""", ] solutions = [ -""" + """ ! This is a Fortran Comment ! this also. """, - -""" + """ A = B * C + D + & EF**2 """, - -""" + """ WRITE(*,*) "Just a regular F77 line." """, - -""" + """ !$OMP PARALLEL DO PRIVATE(X1, X2, & !$OMP X3, X4) ! some code... !$OMP END PARALLEL DO !$OMP """, -""" + """ #define NDEBUG """, -""" + """ CALL FOO(A, B, C, & ! a comment ! another comment D, E, F) """, -""" + """ ! Comment1 ! Comment2 ! Comment3 @@ -156,23 +145,23 @@ C + D WRITE(*,*) "Regular line." """, -""" + """ ! """, -""" + """ !sho """, -""" + """ ! An empty line and an ! continuation character X = SIN(A) * COS(B) + & SIN(B) * COS(A) + & X """, -""" + """ 10 CONTINUE """, -""" + """ ! Test with code that uses text after 72nd column as comments ENDOFLINE E = M * C**2 !COMMENT CALL FUNC(A, B, C, & !COMMENT @@ -180,67 +169,49 @@ ! comment inbetween WHY G, H, I) !SOMETHING """, - -""" + """ 1000 E = & ! an inline comment in a continued line 2 * 3 """, - -""" + """ E = 42 ! inline comment extending beyond column 72 """, - -""" + """ E = 42 ! just a short inline comment """, - -""" + """ C = "!" // & '!' // & !ABC "!'!" // & "A" """, - -""" + """ E = LI !ABC!DEF """, - -""" + """ E = 72 """, - -""" + """ E = LI !Comment E = 72 !Comment """, - -""" + """ E = LI !Comment -""" +""", ] -class Test_CompareStr(unittest.TestCase): - def streamComp(self, stream1, stream2): - for s1, s2 in zip(stream1, stream2): - self.assertEqual(s1, s2) +def assert_streams_equal(stream1, stream2): + for s1, s2 in zip(stream1, stream2): + assert s1 == s2 -def dotest(self, instr, solution): - instream = StringIO(instr) - outstream = StringIO(solution) - self.streamComp(outstream, convertToFree(instream)) + +@pytest.mark.parametrize( + "teststr, expected", zip(teststr, solutions), ids=range(len(solutions)) +) +def test_fixed2free(teststr, expected): + instream = StringIO(teststr) + outstream = StringIO(expected) + assert_streams_equal(outstream, convertToFree(instream)) instream.close() outstream.close() - -def makeTest(instr, solution): - return lambda self: dotest(self, instr, solution) - -if __name__ == "__main__": - num = 0 - for instr, solution in zip(teststr, solutions): - testfun = makeTest(instr, solution) - testfun.__name__ = "test_" + str(num) - setattr(Test_CompareStr, testfun.__name__, testfun) - num += 1 - unittest.main() -