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():
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:
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:
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:
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 <h4>Arguments</h4> <em>None</em>, 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 (a Fortran Parquet I/O library) — 12 generics, 62 specific members total. Live example: parquet_get_metadata — 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 (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.
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 NAMEsubmodule form vs. a fully restatedmodule 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.htmlrenders each generic member viamacros.proc_entry(proc.procedure)(interface.modprocsloop), whereproc.procedureis set duringFortranInterface.correlate():self.all_procs[name]resolves to the submodule'sFortranModuleProcedureImplementationobject for that name (rather than to theFortranSubroutine/FortranFunctionparsed from the standalone interface spec, which does carry real argument info and is what backs a solo public procedure's page).FortranModuleProcedureImplementation._initialize()hardcodes:unconditionally — it never parses dummy-argument declarations from the submodule body at all, regardless of whether that body uses the abbreviated
module procedure NAMEform or fully restatesmodule 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: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 examplesrc/example_impl.f90: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_impldocs.md:Run
ford docs.md, then compare:ford-doc/interface/get_value.html— bothget_value_int32andget_value_real64cards show<h4>Arguments</h4> <em>None</em>, 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
pip show ford)md_extensions = ["markdown.extensions.toc"]not required to reproduce (left out of the minimaldocs.mdabove)Real-world example
This affects every public generic interface in etempel/parquet-fortran (a Fortran Parquet I/O library) — 12 generics, 62 specific members total. Live example:
parquet_get_metadata— 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 insrc/parquet.f90(search formodule 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.