Fortran-FOSS-Programmers / Fortran-FOSS-Programmers/ford

Generic interface members (`module procedure` in a named `interface` block) always show "Arguments: None", regardless of source form

Open
#738 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
458
Forks
138
PR merge metrics
No merged PRs in 30d

Description

## Description

For a named, multi-specific generic interface (`interface NAME ... module procedure X ... end interface`), every member's card under "Module Procedures" on the generic's own page permanently renders **"Arguments: None"** — even when the specific procedure's signature is fully declared and documented, both in its standalone interface spec and in the submodule implementation that provides it.

This is not a doc-comment placement issue: I tried every combination (docmarks on the interface spec only, on the submodule implementation only, on both; abbreviated `module procedure NAME` submodule form vs. a fully restated `module subroutine NAME(args)` submodule form with the argument list and `!!` postdocs repeated). None of it changes the rendered output.

A **solo** public procedure with the exact same argument shape — declared directly in the module's interface block and `public ::`-ed by its own name, i.e. *not* wrapped in a named multi-specific generic — renders its arguments correctly. So this is specific to how generic-interface members are resolved, not to submodule module-procedure parsing in general.

## Root cause (traced in `ford/sourceform.py`, v7.0.13)

- `genint_page.html` renders each generic member via `macros.proc_entry(proc.procedure)` (`interface.modprocs` loop), where `proc.procedure` is set during `FortranInterface.correlate()`:

```python
if self.generic:
for modproc in self.modprocs:
procedure = self.all_procs[modproc.name.lower()]
...
modproc.procedure = procedure
```

- `self.all_procs[name]` resolves to the **submodule's** `FortranModuleProcedureImplementation` object for that name (rather than to the `FortranSubroutine`/`FortranFunction` parsed from the standalone interface spec, which does carry real argument info and is what backs a *solo* public procedure's page).

- `FortranModuleProcedureImplementation._initialize()` hardcodes:

```python
self.attribs: List[str] = []
self.args: List[str] = []
self.retvar = None
```

unconditionally — it never parses dummy-argument declarations from the submodule body at all, regardless of whether that body uses the abbreviated `module procedure NAME` form or fully restates `module subroutine NAME(args)`.

Since the "Module Procedures" list always resolves through this object, the argument table is empty by construction, for every generic in every project, independent of anything the user writes.

## Minimal reproducer

`src/example.f90`:
```fortran
module example
implicit none
private
public :: get_value, get_value_solo

!> Reads back one key's value, dispatched by value's declared kind.
!> `key` is the lookup key; `value` receives the parsed result.
interface get_value
module procedure get_value_int32
module procedure get_value_real64
end interface get_value

interface
!> int32 specific of get_value.
module subroutine get_value_int32(key, value)
character(len=*), intent(in) :: key !! lookup key.
integer, intent(out) :: value !! parsed int32 result.
end subroutine get_value_int32

!> real64 specific of get_value.
module subroutine get_value_real64(key, value)
character(len=*), intent(in) :: key !! lookup key.
real(8), intent(out) :: value !! parsed real64 result.
end subroutine get_value_real64

!> Solo public procedure (NOT a generic member) with the exact same
!> shape as get_value_int32 -- renders its Arguments table correctly.
module subroutine get_value_solo(key, value)
character(len=*), intent(in) :: key !! lookup key.
integer, intent(out) :: value !! parsed int32 result.
end subroutine get_value_solo
end interface
end module example
```

`src/example_impl.f90`:
```fortran
submodule (example) example_impl
implicit none
contains
!> int32 specific of get_value.
module subroutine get_value_int32(key, value)
character(len=*), intent(in) :: key !! lookup key.
integer, intent(out) :: value !! parsed int32 result.
value = len(key)
end subroutine get_value_int32

!> real64 specific of get_value.
module subroutine get_value_real64(key, value)
character(len=*), intent(in) :: key !! lookup key.
real(8), intent(out) :: value !! parsed real64 result.
value = real(len(key), 8)
end subroutine get_value_real64

module subroutine get_value_solo(key, value)
character(len=*), intent(in) :: key !! lookup key.
integer, intent(out) :: value !! parsed int32 result.
value = len(key)
end subroutine get_value_solo
end submodule example_impl
```

`docs.md`:
```
project: example
src_dir: ./src
output_dir: ./ford-doc
display: public
protected

# Example
```

Run `ford docs.md`, then compare:
- `ford-doc/interface/get_value.html` — both `get_value_int32` and `get_value_real64` cards show `

Arguments

None`, despite both dummy arguments being fully declared and documented.
- `ford-doc/interface/get_value_solo.html` — `get_value_solo`, same argument shape, gets its own page with a correctly populated argument table (type, intent, name, description all present).

## Expected behavior

Each generic member's "Arguments" table should show the same information a solo procedure with an identical signature shows — presumably by resolving `proc.procedure` (or building the args list) from the specific's own interface spec / parsed signature rather than (or in addition to) the submodule implementation object.

## Environment

- FORD 7.0.13 (`pip show ford`)
- Reproduced on macOS, Python 3.14
- `md_extensions = ["markdown.extensions.toc"]` not required to reproduce (left out of the minimal `docs.md` above)

## Real-world example

This affects every public generic interface in [etempel/parquet-fortran](https://github.com/etempel/parquet-fortran) (a Fortran Parquet I/O library) — 12 generics, 62 specific members total. Live example: [`parquet_get_metadata`](https://etempel.github.io/parquet-fortran/interface/parquet_get_metadata.html) — the top-level page prose (from the generic's own doc-comment) is present, but every one of the 12 "Module Procedures" cards (`parquet_get_metadata_int32`, `_int64`, `_float32`, ...) shows "Arguments: None", even though each specific's full signature and `!!`-tagged argument docs are written out in [`src/parquet.f90`](https://github.com/etempel/parquet-fortran/blob/main/src/parquet.f90) (search for `module subroutine parquet_get_metadata_int32`). We worked around it on our end by moving the argument-level detail into prose in each generic's own leading doc-comment (the one thing that *does* render), but a structured per-argument table would obviously be preferable.

Related but distinct existing issue: #412 ("tying documentation to a generic name") — same general area (generic interfaces being under-served by FORD's documentation model) but about a different symptom (no way to attach a doc-comment to the generic name from elsewhere), not this specific empty-arguments bug.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in ford/sourceform.py at FortranInterface.correlate() and FortranModuleProcedureImplementation._initialize(), then inspect genint_page.html and its proc_entry(proc.procedure) loop. Trace how generic members resolve to implementation objects instead of parsed interface procedures. Done means the minimal reproducer's generic member cards show populated argument tables matching the standalone procedure, rather than "Arguments: None".

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.