Monday, July 25, 2022

The State of Fortran Generics

I just returned from the joint WG5/J3 meeting (the international and US committees in charge of producing the next revision to the Fortran standard). The Generics subgroup, of which I am a contributing member, had a very successful showing.

The committee discussed and passed 4 “Specification” papers regarding the template feature slated for inclusion in the 202Y revision of the standard. The combination of the papers provide a complete description of the expected semantics of the feature. In this post I will try to summarize and demonstrate with some examples, what this feature will enable, and likely look like.

NOTE: The exact syntax has not been decided. There are many keywords and syntax elements that are still in debate, but the general structure should not change much. It’s entirely likely that the examples shown below will not work unmodified when the standard is finally published.

The four papers passed, and that I will try to summarize in order, define the semantics for

  • A new template construct, including where it may appear, and what may appear within it
  • The scoping rules for templates and their instantiations
  • A new instantiate statement
  • A new restriction block and requires statement

Template Construct

The first thing of note is that a new construct will be available. This will be used to define a template, with specific things being “parameterized”. The things allowed to be template parameters are:

  • types
  • procedures
  • integer, logical or character constants (Note that characters must be assumed length, and arrays must be assumed size or assumed rank)

A template may appear in the specification section of a

A template can then contain any valid Fortran that could be found within a module. I.e. it can define new types, variables, constants and procedures following a contains statement. There is one caveat. All operations and procedure invocations within a template must have explicit interfaces, and for the purposes of checking those interfaces, all deferred types (types that are template parameters) are treated as completely unique. This has the implication that entities of deferred type; can only be assigned to entities declared to be of the same deferred type, can only be passed as actual arguments to procedures who’s corresponding dummy argument is declared to be of the same deferred type. The consequences of such a constraint is that it can be determined a priori that a template will be valid for all actual parameters. The following, somewhat contrived, example illustrates the intended behavior.

type :: u
  ...
end type
...
template tmpl(T, C, S)
  type, deferred :: T
  character(len=*), constant :: C
  interface
    subroutine S(x, y)
      type(T), intent(inout) :: x, y
    end subroutine
  end interface
contains
  function f(x, i)
    type(T), intent(in) :: x
    type(u), intent(in) :: i
    type(T) :: f
    ...
  end function
  subroutine foo(x, u1)
    real, intent(inout) :: x
    type(u), intent(in) :: u1
    type(T) :: t1, t2

    call S(t1, t2) ! Valid
    call S(t1, x)  ! Invalid; x not declared to be of type T
    t1 = f(t2, u1) ! Valid
    x = f(t1, u1) ! Invalid; cannot assign T to real
    t1 = t2 + t2 ! Invalid; + not defined for T + T
  end subroutine
end template

Scoping Rules

The scoping rules for templates are relatively straightforward at this point. A template has host association (i.e. it has access to any entities available in the scope in which it is defined). An instantiation of a template brings into scope only those entities within the template that are public. Thus it will likely be best practice for templates, as many consider it to be for modules, for a template to begin

template tmpl(...)
  private
  public :: ...
  ...
end template

as well as for instantiations to make use of the only clause. I.e.

instantiate tmpl(...), only: ...

Instantiation

A new instantiate statement provides actual parameters for a template, and makes the template entities available. It also provides a rename mechanism to alleviate any potential name conflicts, and an only clause to limit those entities actually brought into scope. There is some complication involved in the underlying mechanism for this however. An instantiate statement is said to refer to an instantiation, and that instantiate statements with identical actual parameters are said to refer to the same instantiation.

The main benefit to this is that types declared within a template and then instantiated in multiple places are the same type where the rules of Fortran might otherwise consider them to be different types. It also means that procedures with save variables, while not advised, will behave as expected when used in separate places (i.e. the “separately instantiated procedures” will refer to the same saved variable). It will be possible to override this behavior, so that an instantiate statement can produce an entirely separate and unique instance. An example to illustrate is shown below.

template wrapper_tmpl(T)
  type, deferred :: T
  type :: wrapper
    type(T) :: wrapped
  end type
end template

instantiate wrapper_tmpl(real), real_w => wrapper
instantiate wrapper_tmpl(real), other_w => wrapper
instantiate, unique :: wrapper_tmpl(real), w1 => wrapper
instantiate, unique :: wrapper_tmpl(real), w2 => wrapper

! Types real_w and other_w are the same type.
! Type w1 is a different type than all of real_w, other_w and w2
! Type w2 is a different type than all of real_w, other_w and w1

For well designed templates and libraries, users shouldn’t have to think about these complexities most of the time. For compiler writers however, this complexity could be tricky. An instantiate statement may need to refer an instance produced by a previously created instantiate statement, in which case it needs to somehow find it, or it may need to create one in such a way that other instantiate statements will be able to find it.

Restriction and Require

Because certain combinations of template parameters are likely to be common, and have meaningful names, another new block and statement have been added to facilitate the naming and reuse of certain template parameter declarations. A restriction block is a new construct, with a name and template parameters, that can contain declarations of template parameters. A requires statement can then appear in a template or restriction block to “include” those declarations. An illustrative example is shown below.

restriction binary_op(T, U, V, binop)
  type, deferred :: T, U, V
  interface
    function binop(x, y) result(z)
      type(T), intent(in) :: x
      type(U), intent(in) :: y
      type(V) :: z
    end function
  end interface
end restriction
...
template tmpl(T, binop)
  requires binary_op(T, T, real, binop)
contains
  function path_length(steps)
    type(T), intent(in) :: steps(:)
    real :: path_length

    integer :: i

    path_length = 0
    do i = 1, size(steps)-1
      path_length = path_length + binop(steps(i), steps(i+1))
    end do
  end function
end template

Conclusion

The semantics of the new template feature have now been established. The committee still has work to do on finalizing the syntax and then making appropriate edits to the standard, but the basic structure and behavior is now clear.

Acknowledgements

I need to acknowledge all those who helped in this effort. There are too many to name them all, but in particular, Tom Clune (of NASA) has done a tremendous job organizing the subgroup and representing the ideas to the committee. The committee has also been very understanding and provided us great constructive feedback.



from Hacker News https://ift.tt/KsvkNgG

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.