Repository: numpy/numpy

Issue:
Wrong pyf with declaration of parameters in modules
f2py provided with numpy 1.12.0 produces wrong pyf signature in case of parameter declaration in modules. Noticed with python2.7.11. Calling f2py by python3.4 doesn't show this issue.

write the following in file.f90:
```
module ppcolloc_data
  implicit none
  integer ( kind = 4 ), parameter :: npiece = 100
  integer ( kind = 4 ), parameter :: ncoef = 2000
  ! old common approx
  integer ( kind = 4 ) l, kpm
  real ( kind = 8 ), dimension(npiece) :: break
  real ( kind = 8 ), dimension(ncoef) :: coef
  save
end module ppcolloc_data
```
f2py file.f90 -m file -h file.pyf

Note the output to screen:
```
            Block: ppcolloc_data
In: :file:file.f90:ppcolloc_data
get_parameters: got "unexpected EOF while parsing (<string>, line 0)" on ''
In: :file:file.f90:ppcolloc_data
get_parameters: got "unexpected EOF while parsing (<string>, line 0)" on ''
```

head file.pyf
shows:
```
!    -*- f90 -*-
! Note: the context of this file is case sensitive.

python module file ! in 
    interface  ! in :file
        module ppcolloc_data ! in :file:file.f90
            real(kind=8) dimension() :: break_bn
            real(kind=8) dimension() :: coef
            integer(kind=4), parameter,optional :: ncoef=2000
            integer(kind=4), parameter,optional :: npiece=100
```
Note that vector dimensions are missing.

lines like:
character (len = 2), parameter, dimension(3) :: x=(/ 'ab', 'bc, 'cd' /)
cannot be treated too, with both py2.7 and py3.4.

Thanks in advance for your support.


Discussion:
Using Python 3 and
```
Version:     2
numpy Version: 1.16.1
```

I cannot reproduce the first error.

Parsing ` character (len = 2), parameter, dimension(3) :: x=(/ 'ab', 'bc, 'cd' /)` fails but I am not sure that this is supported by f2py anyway.

Resolving patch:
diff --git a/numpy/f2py/crackfortran.py b/numpy/f2py/crackfortran.py
--- a/numpy/f2py/crackfortran.py
+++ b/numpy/f2py/crackfortran.py
@@ -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
@@ -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 = []
@@ -2487,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)
 
@@ -2495,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('_')
@@ -2529,6 +2551,10 @@ 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):
@@ -2536,11 +2562,11 @@ def get_parameters(vars, global_params={}):
             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()
@@ -2548,8 +2574,7 @@ def get_parameters(vars, global_params={}):
                 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
 
 
@@ -2558,6 +2583,7 @@ def _eval_length(length, params):
         return '(*)'
     return _eval_scalar(length, params)
 
+
 _is_kind_number = re.compile(r'\d+_').match
 
 
@@ -2578,6 +2604,10 @@ def _eval_scalar(value, params):
 
 
 def analyzevars(block):
+    """
+    Sets correct dimension information for each variable/parameter
+    """
+
     global f90modulevars
 
     setmesstext(block)
@@ -2606,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()):
@@ -2705,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)
@@ -2959,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)


Test patch:
diff --git a/numpy/f2py/tests/src/parameter/constant_array.f90 b/numpy/f2py/tests/src/parameter/constant_array.f90
new file mode 100644
--- /dev/null
+++ b/numpy/f2py/tests/src/parameter/constant_array.f90
@@ -0,0 +1,45 @@
+! Check that parameter arrays are correctly intercepted.
+subroutine foo_array(x, y, z)
+  implicit none
+  integer, parameter :: dp = selected_real_kind(15)
+  integer, parameter :: pa = 2
+  integer, parameter :: intparamarray(2) = (/ 3, 5 /)
+  integer, dimension(pa), parameter :: pb = (/ 2, 10 /)
+  integer, parameter, dimension(intparamarray(1)) :: pc = (/ 2, 10, 20 /)
+  real(dp), parameter :: doubleparamarray(3) = (/ 3.14_dp, 4._dp, 6.44_dp /)
+  real(dp), intent(inout) :: x(intparamarray(1))
+  real(dp), intent(inout) :: y(intparamarray(2))
+  real(dp), intent(out) :: z
+
+  x = x/pb(2)
+  y = y*pc(2)
+  z = doubleparamarray(1)*doubleparamarray(2) + doubleparamarray(3)
+
+  return
+end subroutine
+
+subroutine foo_array_any_index(x, y)
+  implicit none
+  integer, parameter :: dp = selected_real_kind(15)
+  integer, parameter, dimension(-1:1) :: myparamarray = (/ 6, 3, 1 /)
+  integer, parameter, dimension(2) :: nested = (/ 2, 0 /)
+  integer, parameter :: dim = 2
+  real(dp), intent(in) :: x(myparamarray(-1))
+  real(dp), intent(out) :: y(nested(1), myparamarray(nested(dim)))
+
+  y = reshape(x, (/nested(1), myparamarray(nested(2))/))
+
+  return
+end subroutine
+
+subroutine foo_array_delims(x)
+  implicit none
+  integer, parameter :: dp = selected_real_kind(15)
+  integer, parameter, dimension(2) :: myparamarray = (/ (6), 1 /)
+  integer, parameter, dimension(3) :: test = (/2, 1, (3)/)
+  real(dp), intent(out) :: x
+
+  x = myparamarray(1)+test(3)
+
+  return
+end subroutine
diff --git a/numpy/f2py/tests/test_crackfortran.py b/numpy/f2py/tests/test_crackfortran.py
--- a/numpy/f2py/tests/test_crackfortran.py
+++ b/numpy/f2py/tests/test_crackfortran.py
@@ -190,6 +190,9 @@ class TestDimSpec(util.F2PyTest):
         ! the value of n is computed in f2py wrapper
         !f2py intent(out) n
         integer, dimension({dimspec}), intent(in) :: a
+        if (a({first}).gt.0) then
+          ! print*, "a=", a
+        endif
       end subroutine
     """)
 
@@ -323,7 +326,6 @@ def test_nameargspattern_backtracking(self, adversary):
             good_version_of_adversary = repeated_adversary + '@)@'
             assert nameargspattern.search(good_version_of_adversary)
 
-
 class TestFunctionReturn(util.F2PyTest):
     sources = [util.getpath("tests", "src", "crackfortran", "gh23598.f90")]
 
@@ -348,3 +350,55 @@ def test_gh22648(self, tmp_path):
         with contextlib.redirect_stdout(io.StringIO()) as stdout_f2py:
             mod = crackfortran.crackfortran([str(fpath)])
         assert "Mismatch" not in stdout_f2py.getvalue()
+
+class TestParamEval():
+    # issue gh-11612, array parameter parsing
+    def test_param_eval_nested(self):
+        v = '(/3.14, 4./)'
+        g_params = dict(kind=crackfortran._kind_func,
+                selected_int_kind=crackfortran._selected_int_kind_func,
+                selected_real_kind=crackfortran._selected_real_kind_func)
+        params = {'dp': 8, 'intparamarray': {1: 3, 2: 5},
+                  'nested': {1: 1, 2: 2, 3: 3}}
+        dimspec = '(2)'
+        ret = crackfortran.param_eval(v, g_params, params, dimspec=dimspec)
+        assert ret == {1: 3.14, 2: 4.0}
+
+    def test_param_eval_nonstandard_range(self):
+        v = '(/ 6, 3, 1 /)'
+        g_params = dict(kind=crackfortran._kind_func,
+                selected_int_kind=crackfortran._selected_int_kind_func,
+                selected_real_kind=crackfortran._selected_real_kind_func)
+        params = {}
+        dimspec = '(-1:1)'
+        ret = crackfortran.param_eval(v, g_params, params, dimspec=dimspec)
+        assert ret == {-1: 6, 0: 3, 1: 1}
+
+    def test_param_eval_empty_range(self):
+        v = '6'
+        g_params = dict(kind=crackfortran._kind_func,
+                selected_int_kind=crackfortran._selected_int_kind_func,
+                selected_real_kind=crackfortran._selected_real_kind_func)
+        params = {}
+        dimspec = ''
+        pytest.raises(ValueError, crackfortran.param_eval, v, g_params, params,
+                      dimspec=dimspec)
+
+    def test_param_eval_non_array_param(self):
+        v = '3.14_dp'
+        g_params = dict(kind=crackfortran._kind_func,
+                selected_int_kind=crackfortran._selected_int_kind_func,
+                selected_real_kind=crackfortran._selected_real_kind_func)
+        params = {}
+        ret = crackfortran.param_eval(v, g_params, params, dimspec=None)
+        assert ret == '3.14_dp'
+
+    def test_param_eval_too_many_dims(self):
+        v = 'reshape((/ (i, i=1, 250) /), (/5, 10, 5/))'
+        g_params = dict(kind=crackfortran._kind_func,
+                selected_int_kind=crackfortran._selected_int_kind_func,
+                selected_real_kind=crackfortran._selected_real_kind_func)
+        params = {}
+        dimspec = '(0:4, 3:12, 5)'
+        pytest.raises(ValueError, crackfortran.param_eval, v, g_params, params,
+                      dimspec=dimspec)
diff --git a/numpy/f2py/tests/test_parameter.py b/numpy/f2py/tests/test_parameter.py
--- a/numpy/f2py/tests/test_parameter.py
+++ b/numpy/f2py/tests/test_parameter.py
@@ -14,6 +14,7 @@ class TestParameters(util.F2PyTest):
         util.getpath("tests", "src", "parameter", "constant_both.f90"),
         util.getpath("tests", "src", "parameter", "constant_compound.f90"),
         util.getpath("tests", "src", "parameter", "constant_non_compound.f90"),
+        util.getpath("tests", "src", "parameter", "constant_array.f90"),
     ]
 
     @pytest.mark.slow
@@ -110,3 +111,21 @@ def test_constant_sum(self):
         x = np.arange(3, dtype=np.float64)
         self.module.foo_sum(x)
         assert np.allclose(x, [0 + 1 * 3 * 3 + 2 * 3 * 3, 1 * 3, 2 * 3])
+
+    def test_constant_array(self):
+        x = np.arange(3, dtype=np.float64)
+        y = np.arange(5, dtype=np.float64)
+        z = self.module.foo_array(x, y)
+        assert np.allclose(x, [0.0, 1./10, 2./10])
+        assert np.allclose(y, [0.0, 1.*10, 2.*10, 3.*10, 4.*10])
+        assert np.allclose(z, 19.0)
+
+    def test_constant_array_any_index(self):
+        x = np.arange(6, dtype=np.float64)
+        y = self.module.foo_array_any_index(x)
+        assert np.allclose(y, x.reshape((2, 3), order='F'))
+
+    def test_constant_array_delims(self):
+        x = self.module.foo_array_delims()
+        assert x == 9
+