Skip to content

Commit

Permalink
Merge pull request #25491 from charris/backport-f2py-toplevel-hard
Browse files Browse the repository at this point in the history
MAINT: Update crackfortran.py and f2py2e.py from main
  • Loading branch information
charris committed Dec 24, 2023
2 parents 296dd8b + 6b270fb commit 08dac26
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 87 deletions.
230 changes: 200 additions & 30 deletions numpy/f2py/crackfortran.py
Expand Up @@ -993,6 +993,16 @@ def _resolvenameargspattern(line):


def analyzeline(m, case, line):
"""
Reads each line in the input file in sequence and updates global vars.
Effectively reads and collects information from the input file to the
global variable groupcache, a dictionary containing info about each part
of the fortran module.
At the end of analyzeline, information is filtered into the correct dict
keys, but parameter values and dimensions are not yet interpreted.
"""
global groupcounter, groupname, groupcache, grouplist, filepositiontext
global currentfilename, f77modulename, neededinterface, neededmodule
global expectbegin, gotnextfile, previous_context
Expand Down Expand Up @@ -1679,10 +1689,18 @@ def markinnerspaces(line):


def updatevars(typespec, selector, attrspec, entitydecl):
"""
Returns last_name, the variable name without special chars, parenthesis
or dimension specifiers.
Alters groupcache to add the name, typespec, attrspec (and possibly value)
of current variable.
"""
global groupcache, groupcounter

last_name = None
kindselect, charselect, typename = cracktypespec(typespec, selector)
# Clean up outer commas, whitespace and undesired chars from attrspec
if attrspec:
attrspec = [x.strip() for x in markoutercomma(attrspec).split('@,@')]
l = []
Expand Down Expand Up @@ -2396,8 +2414,6 @@ def _calc_depend_dict(vars):


def get_sorted_names(vars):
"""
"""
depend_dict = _calc_depend_dict(vars)
names = []
for name in list(depend_dict.keys()):
Expand Down Expand Up @@ -2450,7 +2466,7 @@ def _selected_real_kind_func(p, r=0, radix=0):
if p < 16:
return 8
machine = platform.machine().lower()
if machine.startswith(('aarch64', 'alpha', 'arm64', 'loongarch', 'power', 'ppc', 'riscv', 's390x', 'sparc')):
if machine.startswith(('aarch64', 'alpha', 'arm64', 'loongarch', 'mips', 'power', 'ppc', 'riscv', 's390x', 'sparc')):
if p <= 33:
return 16
else:
Expand Down Expand Up @@ -2489,6 +2505,7 @@ def get_parameters(vars, global_params={}):
# TODO: test .eq., .neq., etc replacements.
]:
v = v.replace(*repl)

v = kind_re.sub(r'kind("\1")', v)
v = selected_int_kind_re.sub(r'selected_int_kind(\1)', v)

Expand All @@ -2497,14 +2514,17 @@ def get_parameters(vars, global_params={}):
# then we may easily remove those specifiers.
# However, it may be that the user uses other specifiers...(!)
is_replaced = False

if 'kindselector' in vars[n]:
# Remove kind specifier (including those defined
# by parameters)
if 'kind' in vars[n]['kindselector']:
orig_v_len = len(v)
v = v.replace('_' + vars[n]['kindselector']['kind'], '')
# Again, this will be true if even a single specifier
# has been replaced, see comment above.
is_replaced = len(v) < orig_v_len

if not is_replaced:
if not selected_kind_re.match(v):
v_ = v.split('_')
Expand All @@ -2531,27 +2551,30 @@ def get_parameters(vars, global_params={}):
outmess(f'get_parameters[TODO]: '
f'implement evaluation of complex expression {v}\n')

dimspec = ([s.lstrip('dimension').strip()
for s in vars[n]['attrspec']
if s.startswith('dimension')] or [None])[0]

# Handle _dp for gh-6624
# Also fixes gh-20460
if real16pattern.search(v):
v = 8
elif real8pattern.search(v):
v = 4
try:
params[n] = eval(v, g_params, params)

params[n] = param_eval(v, g_params, params, dimspec=dimspec)
except Exception as msg:
params[n] = v
outmess('get_parameters: got "%s" on %s\n' % (msg, repr(v)))
outmess(f'get_parameters: got "{msg}" on {n!r}\n')

if isstring(vars[n]) and isinstance(params[n], int):
params[n] = chr(params[n])
nl = n.lower()
if nl != n:
params[nl] = params[n]
else:
print(vars[n])
outmess(
'get_parameters:parameter %s does not have value?!\n' % (repr(n)))
outmess(f'get_parameters:parameter {n!r} does not have value?!\n')
return params


Expand All @@ -2560,6 +2583,7 @@ def _eval_length(length, params):
return '(*)'
return _eval_scalar(length, params)


_is_kind_number = re.compile(r'\d+_').match


Expand All @@ -2580,6 +2604,10 @@ def _eval_scalar(value, params):


def analyzevars(block):
"""
Sets correct dimension information for each variable/parameter
"""

global f90modulevars

setmesstext(block)
Expand Down Expand Up @@ -2608,7 +2636,8 @@ def analyzevars(block):
svars.append(n)

params = get_parameters(vars, get_useparameters(block))

# At this point, params are read and interpreted, but
# the params used to define vars are not yet parsed
dep_matches = {}
name_match = re.compile(r'[A-Za-z][\w$]*').match
for v in list(vars.keys()):
Expand Down Expand Up @@ -2707,27 +2736,30 @@ def analyzevars(block):
check = None
if dim and 'dimension' not in vars[n]:
vars[n]['dimension'] = []
for d in rmbadname([x.strip() for x in markoutercomma(dim).split('@,@')]):
star = ':' if d == ':' else '*'
for d in rmbadname(
[x.strip() for x in markoutercomma(dim).split('@,@')]
):
# d is the expression inside the dimension declaration
# Evaluate `d` with respect to params
if d in params:
d = str(params[d])
for p in params:
re_1 = re.compile(r'(?P<before>.*?)\b' + p + r'\b(?P<after>.*)', re.I)
m = re_1.match(d)
while m:
d = m.group('before') + \
str(params[p]) + m.group('after')
m = re_1.match(d)

if d == star:
dl = [star]
try:
# the dimension for this variable depends on a
# previously defined parameter
d = param_parse(d, params)
except (ValueError, IndexError, KeyError):
outmess(
('analyzevars: could not parse dimension for '
f'variable {d!r}\n')
)

dim_char = ':' if d == ':' else '*'
if d == dim_char:
dl = [dim_char]
else:
dl = markoutercomma(d, ':').split('@:@')
if len(dl) == 2 and '*' in dl: # e.g. dimension(5:*)
dl = ['*']
d = '*'
if len(dl) == 1 and dl[0] != star:
if len(dl) == 1 and dl[0] != dim_char:
dl = ['1', dl[0]]
if len(dl) == 2:
d1, d2 = map(symbolic.Expr.parse, dl)
Expand Down Expand Up @@ -2961,9 +2993,152 @@ def compute_deps(v, deps):
del vars[n]
return vars


analyzeargs_re_1 = re.compile(r'\A[a-z]+[\w$]*\Z', re.I)


def param_eval(v, g_params, params, dimspec=None):
"""
Creates a dictionary of indices and values for each parameter in a
parameter array to be evaluated later.
WARNING: It is not possible to initialize multidimensional array
parameters e.g. dimension(-3:1, 4, 3:5) at this point. This is because in
Fortran initialization through array constructor requires the RESHAPE
intrinsic function. Since the right-hand side of the parameter declaration
is not executed in f2py, but rather at the compiled c/fortran extension,
later, it is not possible to execute a reshape of a parameter array.
One issue remains: if the user wants to access the array parameter from
python, we should either
1) allow them to access the parameter array using python standard indexing
(which is often incompatible with the original fortran indexing)
2) allow the parameter array to be accessed in python as a dictionary with
fortran indices as keys
We are choosing 2 for now.
"""
if dimspec is None:
try:
p = eval(v, g_params, params)
except Exception as msg:
p = v
outmess(f'param_eval: got "{msg}" on {v!r}\n')
return p

# This is an array parameter.
# First, we parse the dimension information
if len(dimspec) < 2 or dimspec[::len(dimspec)-1] != "()":
raise ValueError(f'param_eval: dimension {dimspec} can\'t be parsed')
dimrange = dimspec[1:-1].split(',')
if len(dimrange) == 1:
# e.g. dimension(2) or dimension(-1:1)
dimrange = dimrange[0].split(':')
# now, dimrange is a list of 1 or 2 elements
if len(dimrange) == 1:
bound = param_parse(dimrange[0], params)
dimrange = range(1, int(bound)+1)
else:
lbound = param_parse(dimrange[0], params)
ubound = param_parse(dimrange[1], params)
dimrange = range(int(lbound), int(ubound)+1)
else:
raise ValueError(f'param_eval: multidimensional array parameters '
'{dimspec} not supported')

# Parse parameter value
v = (v[2:-2] if v.startswith('(/') else v).split(',')
v_eval = []
for item in v:
try:
item = eval(item, g_params, params)
except Exception as msg:
outmess(f'param_eval: got "{msg}" on {item!r}\n')
v_eval.append(item)

p = dict(zip(dimrange, v_eval))

return p


def param_parse(d, params):
"""Recursively parse array dimensions.
Parses the declaration of an array variable or parameter
`dimension` keyword, and is called recursively if the
dimension for this array is a previously defined parameter
(found in `params`).
Parameters
----------
d : str
Fortran expression describing the dimension of an array.
params : dict
Previously parsed parameters declared in the Fortran source file.
Returns
-------
out : str
Parsed dimension expression.
Examples
--------
* If the line being analyzed is
`integer, parameter, dimension(2) :: pa = (/ 3, 5 /)`
then `d = 2` and we return immediately, with
>>> d = '2'
>>> param_parse(d, params)
2
* If the line being analyzed is
`integer, parameter, dimension(pa) :: pb = (/1, 2, 3/)`
then `d = 'pa'`; since `pa` is a previously parsed parameter,
and `pa = 3`, we call `param_parse` recursively, to obtain
>>> d = 'pa'
>>> params = {'pa': 3}
>>> param_parse(d, params)
3
* If the line being analyzed is
`integer, parameter, dimension(pa(1)) :: pb = (/1, 2, 3/)`
then `d = 'pa(1)'`; since `pa` is a previously parsed parameter,
and `pa(1) = 3`, we call `param_parse` recursively, to obtain
>>> d = 'pa(1)'
>>> params = dict(pa={1: 3, 2: 5})
>>> param_parse(d, params)
3
"""
if "(" in d:
# this dimension expression is an array
dname = d[:d.find("(")]
ddims = d[d.find("(")+1:d.rfind(")")]
# this dimension expression is also a parameter;
# parse it recursively
index = int(param_parse(ddims, params))
return str(params[dname][index])
elif d in params:
return str(params[d])
else:
for p in params:
re_1 = re.compile(
r'(?P<before>.*?)\b' + p + r'\b(?P<after>.*)', re.I
)
m = re_1.match(d)
while m:
d = m.group('before') + \
str(params[p]) + m.group('after')
m = re_1.match(d)
return d


def expr2name(a, block, args=[]):
orig_a = a
a_is_expr = not analyzeargs_re_1.match(a)
Expand Down Expand Up @@ -3216,11 +3391,6 @@ def true_intent_list(var):


def vars2fortran(block, vars, args, tab='', as_interface=False):
"""
TODO:
public sub
...
"""
setmesstext(block)
ret = ''
nout = []
Expand Down

0 comments on commit 08dac26

Please sign in to comment.