-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpreducer.py
More file actions
executable file
·261 lines (246 loc) · 11.4 KB
/
preducer.py
File metadata and controls
executable file
·261 lines (246 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#!/usr/bin/env python3
"""
Python script to downgrade a double precision Fortran routine to single
(real4) precision without changing its external signature, for example
to study the effect of reduced precision arithmetic within only one
subroutine that is part of a larger piece of software.
The script takes the following arguments:
1. The file name to be read. A will be created with _real4 appended
to the name.
2. An optional argument specifying the name of the subroutine that is
to be treated. If the argument is not given, all subroutines in
the file will be modified.
Limitations:
- Currently only F77 files are supported. This could be easily fixed by
using the fortran.two parser for F90 files and adjusting some of the
node types, e.g. not only test for fparser.one.statements.Assignment
but also for f2003.Assignment_Stmt
- Currently only files with one subroutine and nothing else are
supported. No modules, no classes, nothing. This could also easily
fixed, by discovering subroutine nodes in a recursive AST search just
like what is already happening to discover assignments
- Whether a variable is first written or first read is determined
lexicographically, not by building the flow graph. This means that
branches or even goto statements can trick this analysis. It should
still be enough for cases where variables are either read-only or
write-only. Fixing this would be more difficult.
- Read and write access to variables is only detected in assignments,
for example in 'foo(i) = bar + z(g)' we would detect 'foo' as being
written, and 'bar', 'z' as being read. No other access is detected,
for example a subroutine or function call would not result in the
arguments being added to the read and write lists.
"""
import fparser.one.parsefortran
import fparser.common.readfortran
import sys, re
import textwrap
# Read arguments
filename = sys.argv[1]
verbose = False
if(sys.argv[1] == '-verbose'):
filename = sys.argv[2]
verbose = True
def printv(arg):
if verbose:
print(arg)
filename_preduced = "%s_preduced.f"%(filename[0:-2])
unitname = None
if(len(sys.argv)>3):
unitname = sys.argv[2]
if(sys.argv[1] == '-verbose'):
unitname = sys.argv[3]
if(unitname == None):
printv("preducer downgrading the precision of all subroutines in file %s."%(filename))
else:
printv("preducer downgrading the precision of subroutine \"%s\" in file %s."%(unitname,filename))
def cleanVariableName(var):
"""
A reference to a variable can be something messy like "BaR(3,foo),", where
all we want is the variable name "bar". This function removes array
indices, trailing commas etc, and makes everything lowercase, to get only
a clean variable name and nothing else.
"""
return re.split('[,(]',var)[0].lower()
def find_vars(varstring):
current_varname = ''
varlist = list()
parentheses_depth = 0
for i, c in enumerate(varstring):
if c == '(':
parentheses_depth += 1
elif c == ')':
parentheses_depth -= 1
elif parentheses_depth == 0:
if c != ' ' and c != '\t':
if(c == ','):
if(len(current_varname.strip())>0):
varlist.append(current_varname)
current_varname = ''
else:
current_varname += c
varlist.append(current_varname)
if(varlist[0].lower()=='doubleprecision'):
del(varlist[0])
else:
varlist[0] = varlist[0][15:]
return varlist
def visitDoublePrecisionStmt(node):
"""
The f77 parser treats a line containing a double precision variable
declaration as a Line, which is a string of characters. We need to extract
the variable names from that string, and not get confused by arrays. For
example, "double precision foo(a,3), bar" should give us the variables
"foo" and "bar", and nothing else.
"""
if(type(node)!=fparser.one.typedecl_statements.DoublePrecision):
raise Exception("visitDoublePrecisionStmt called on wrong node type")
slist = find_vars(node.item.line)
varset = set()
for s in slist:
varname = cleanVariableName(s)
varset.add(varname) # add this variable name to set
return varset
def visitNode(node,doublevars,doublevars_modified):
"""
Recursively go through the AST and find all assignments.
This is needed to find variables that are read before modified, and
variables that are modified at all.
"""
children = []
doublevars_predefined = set()
if hasattr(node, "content"):
children = node.content
elif hasattr(node, "items"):
children = node.items
elif type(node) in (tuple, list):
children = node
for child in children:
if(type(child)==fparser.one.statements.Assignment):
lhs = cleanVariableName(child.variable)
# Visit an assignment statement, e.g. "a = b + c"
if(lhs in doublevars):
doublevars_modified.add(lhs)
rhs = child.expr
readDoubleVars = set(filter(lambda x: x in rhs, doublevars))
doublevars_predefined = doublevars_predefined.union(readDoubleVars.difference(doublevars_modified))
else:
newmodified, newpredefined = visitNode(child, doublevars, doublevars_modified)
doublevars_modified = doublevars_modified.union(newmodified)
doublevars_predefined = doublevars_predefined.union(newpredefined)
return doublevars_modified, doublevars_predefined
def f77linebreaks(instr):
"""
Takes a string as an input, and breaks all lines after at most 72
characters, using F77 line continuation markers.
"""
outstr = ''
for l in instr.splitlines():
if(len(l.strip())==0): # empty line
outstr += l+'\n'
elif(l[0]!=' ' or l.lstrip()[0]=='!'): # comment line, never touch those
outstr += l+'\n'
else:
if(len(l) > 7 and l[0:7].strip().isnumeric()): # workaround for parser bug: numeric line labels are printed with an incorrect blank space in column 1. Remove this.
l = l[0:7].strip().ljust(7) + l[7:]
while(len(l) > 72):
outstr += l[0:71]+'\n'
l = ' *'+l[71:]
outstr += l+'\n'
return outstr
def real4subroutine(unit, file, allunits):
# Analysis part: Find the subroutine that needs to be modified,
# and for that subroutine, find the double precision arguments
# and for each of those, find out whether they are in/outputs.
args = unit.args.copy()
if(unit.blocktype == 'function'):
args.append(unit.name)
printv(args)
doublevars = set() # all double precision variables declared within subroutine
doublevars_predefined = set() # all double precision variables read before being modified
doublevars_modified = set() # all double precision variables modified within subroutine
decls = list()
for c in unit.content:
decltypes = [fparser.one.typedecl_statements.Byte,
fparser.one.typedecl_statements.Character,
fparser.one.typedecl_statements.Complex,
fparser.one.typedecl_statements.DoubleComplex,
fparser.one.typedecl_statements.DoublePrecision,
fparser.one.typedecl_statements.Integer,
fparser.one.typedecl_statements.Logical,
fparser.one.typedecl_statements.Real,
fparser.one.statements.Parameter]
if(type(c) in decltypes):
decls.append(c)
if(type(c) == fparser.one.typedecl_statements.DoublePrecision):
doublevars = doublevars.union(visitDoublePrecisionStmt(c))
else:
newmodified, newpredefined = visitNode(c, doublevars, doublevars_modified)
doublevars_modified = doublevars_modified.union(newmodified)
doublevars_predefined = doublevars_predefined.union(newpredefined)
doubleargs_modified = doublevars_modified.intersection(args)
doubleargs_predefined = doublevars_predefined.intersection(args)
printv("local double precision variables: %s"%doublevars.difference(args).__str__())
printv("double precision arguments: %s"%doublevars.intersection(args).__str__())
printv(" - modified: %s"%(doubleargs_modified.__str__()))
printv(" - input: %s"%(doubleargs_predefined.__str__()))
printv(" - unused: %s"%(doublevars.intersection(args).difference(doubleargs_predefined.union(doubleargs_modified)).__str__()))
# Cloning part: Create a subroutine that has the same body as the original
# one, but uses the new precision throughout and append _sp to its name
fclone = unit.tofortran()
fclone = fclone.replace('DOUBLEPRECISION','REAL')
if(unit.blocktype == 'function'):
fclone = re.sub('FUNCTION %s'%unit.name,'FUNCTION %s_sp'%unit.name, fclone, flags=re.IGNORECASE)
else:
fclone = re.sub('SUBROUTINE %s'%unit.name,'SUBROUTINE %s_sp'%unit.name, fclone, flags=re.IGNORECASE)
for otherunit in allunits:
fclone = re.sub('CALL %s\('%otherunit.name, 'CALL %s_sp('%otherunit.name, fclone, flags=re.IGNORECASE)
fclone = re.sub('1.0d308', '1.0e38', fclone, flags=re.IGNORECASE)
fclone = f77linebreaks(fclone)
file.write(fclone)
file.write('\n\n')
# Wrapper part: Create a subroutine that has the signature of the original
# one, and performs the down-cast/call/up-cast to the reduced precision
# subroutine.
args_str = ", ".join(unit.args)
args_sp = args_str
for dv in doublevars:
args_sp = re.sub(r"\b%s\b" % dv , '%s_sp'%dv, args_sp)
decls_sp = list()
for d in decls:
if(type(d) == fparser.one.typedecl_statements.DoublePrecision):
varnames = visitDoublePrecisionStmt(d)
d_sp = d.item.line.replace('DOUBLE PRECISION','REAL').lower()
for vn in varnames:
d_sp = re.sub(r"\b%s\b" % vn , '%s_sp'%vn, d_sp)
decls_sp.append(d_sp)
decls_sp.append(d.item.line)
decls_sp = "\n".join(decls_sp)
copyin = set()
for dm in doubleargs_predefined:
copyin.add("%s_sp = %s"%(dm,dm))
copyin = "\n".join(copyin)
copyout = set()
for dm in doubleargs_modified:
copyout.add("%s = %s_sp"%(dm,dm))
copyout = "\n".join(copyout)
if(unit.blocktype == 'function'):
wrapper = "double precision function %s(%s)\n%s\n%s\n%s = %s_sp(%s)\n%s\nreturn\nend function"%(unit.name,args_str,decls_sp,copyin,unit.name,unit.name,args_sp,copyout)
else:
wrapper = "subroutine %s(%s)\n%s\n%s\ncall %s_sp(%s)\n%s\nend subroutine"%(unit.name,args_str,decls_sp,copyin,unit.name,args_sp,copyout)
wrapper = f77linebreaks(textwrap.indent(wrapper,7*' '))
file.write(wrapper)
# Parse Fortran file
reader = fparser.common.readfortran.FortranFileReader(filename)
fp = fparser.one.parsefortran.FortranParser(reader)
fp.parse()
if(len(fp.block.content) == 0):
print("Warning: Preducer called on empty file %s"%(filename))
from shutil import copyfile
copyfile(filename, filename_preduced)
exit()
with open(filename_preduced,'w') as file:
for unit in fp.block.content:
if(unit.blocktype != 'subroutine' and unit.blocktype != 'function'):
raise Exception("Top Unit is neither subroutine nor function")
if(unitname == None or unit.name == unitname):
real4subroutine(unit, file, fp.block.content)