Skip to content

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

Description

@etempel

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.htmlget_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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions