Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for general-use templates #29

Open
arjenmarkus opened this issue Oct 21, 2019 · 49 comments
Open

Proposal for general-use templates #29

arjenmarkus opened this issue Oct 21, 2019 · 49 comments
Labels
Clause 7 Standard Clause 7: Types

Comments

@arjenmarkus
Copy link

Generic features and the apparant lack of them in Fortran (up to the 2018 standard) are a widely discussed subject. I have written a note on the use of modules to achieve a (limted) form of genericity and that has inspired me to a new note which contains a concrete proposal for general-use templates. See the attachment.

Note: this note has been extended after discussion with Magne Haveraaen an Damian Rouson. I realise that the ideas may not have been formulated as clearly as required.

proposal_templates.pdf

@cmacmackin
Copy link

Interesting. I was thinking of something similar to your generic types and contracts actually. I took inspiration from the experimental concept feature in the new language Nim: https://nim-lang.org/docs/manual_experimental.html#concepts

@aradi
Copy link
Contributor

aradi commented Oct 21, 2019

Nice proposal Arjen! One comment: this would require a separate module for every type, rank and attribute combination, one would like to generate. Imagine, you would like to use a list with 5 different types and 3 different ranks each in your project. You would have to create 15 modules... Then, I would still resort to pre-processors offering branches (e.g. Fypp 😉 ) in order to make it automatic, so we would be still stuck in the dark area of pre-processors. 😄

Would it be possible to generalize the syntax in a way, that a user can generate the template for several different types within one module?

@arjenmarkus
Copy link
Author

@cmacmackin : I knew of NIM, never had a look at it though. I have now printed the document (being old-fashioned I do prefer reading from paper ;)).
@aradi : I should make that clearer, While I have modelled this proposal on templates, it is not meant to generate individual modules only. The one thing you need to take care of when putting several instantiations into one module is to make sure that types (and variables and ...) have unique names. Hm, intriguing: should that be a problem to be solved by designging the language feature in that way or by the user (on the principle of "caveat emptor")?

@arjenmarkus
Copy link
Author

The nice thing about this kind of language feature is that you can easily experiment with it (via preprocessing of course) but still.

@cmacmackin
Copy link

Taking another look at this, something which I think is missing is the ability to define procedures which operate on two distinct types which satisfy the generic type. I suppose one way to approach this would be to simply define multiple generic types with the same contract but different names. That would work fine for types without any required components or type-bound procedures but would become repetitive for more complex generic types.

Another issue is that this approach only allows you to define the components and type-bound procedures required for the generic type. You can't require any non-type-bound procedures to take it as an argument.

Finally, any (non-type-bound) procedures would have to be renames if they are to share a scope with another instantiation of the template. I'd suggest some means of automatically making this happen and creating a generic interface for the procedures.

@arjenmarkus
Copy link
Author

When I wrote it, it seemed fairly straightforward - apart from a few bits and pieces like the "implicit" statement maybe ending up in the wrong place and complications regarding "type(data_type)" and plain "data_type". But you have uncovered another issue indeed.

I would like to avoid unnecessary verbosity, as that would hinder the use of such a feature. Thanks, I will definitely revise my proposal :).

@cmacmackin
Copy link

Another question: how would this proposal interact with parameterised derived types? While I don't particularly like how those work, they are a part of the standard and we have to deal with them. Can your generic types be parameterised? We'd have to think through how that could work.

Another issue which I see is that you are treating kind/len (possibly rank, size, and other attributes as well?) as though they are simply part of type. However, they are different things. I'm not convinced that it is appropriate to replace a map a type/kind combo to a generic type, as you show in a number of your examples. I feel like your approach comes too much from the perspective of substituting bits of syntax in such a way to ensure the code compiles. However, I'm not convinced that it is semantically appropriate for Fortran.

@aradi
Copy link
Contributor

aradi commented Oct 21, 2019

I think, we need a syntax which immediately allows the compiler to generate unique names via name mangling, so that user should not have to care more about it, as with normal generic routines. What do you think about something along the following:

module swaptemplate<T, R>
  generics, type :: T
  generics, rank :: R
  implicit none
  private

  public :: swap

  ! This should be generated by the compiler probably
  interface swap
    module procedure swap<T,R>
  end interface swap

contains

  subroutine swap<T, R>(arg1, arg2)
    <T>, dimension(<R>) :: arg1
    <T>, dimension(<R>) :: arg2

    <T>, dimension(<R>), allocatable :: buffer

    allocate(buffer, source=arg1)
    arg1 = arg2
    arg2 = buffer

  end subroutine swap<T, R>

end module swaptemplate


program test_swap
  use swaptemplate<integer, 0>
  use swaptemplate<integer, 1>
  use swaptemplate<real, 0>
  use swaptemplate<real, 1>
  implicit none


  real :: r0a, r0b, r0c
  real :: r1a(2), r1b(2), r1c(2)
  real :: i0a, i0b, i0c
  real :: i1a(2), i1b(2), i1c(2)


  call swap(r0a, r0b, r0c)
  call swap(r1a, r1b, r1c)
  call swap(i0a, i0b, i0c)
  call swap(i1a, i1b, i1c)

end program test_swap

@cmacmackin
Copy link

I agree, something like that seems sensible (although we could debate whether the compiler should automatically generate the interface swap block). This is similar to the old proposal for parameterised modules, which I always liked.

@arjenmarkus
Copy link
Author

Valid questions and useful suggestions. My, there is a lot to think about, isn't there?

How about this to avoid repetition:

type, generic :: data_type
... properties
end type data_type
type, copy(data_type) :: new_type
... possibly additional properties
end type

"copy(...)" would be akin to "extends(...)" in syntax, not in semantics

@aradi
Copy link
Contributor

aradi commented Oct 21, 2019

@cmacmackin Yes, indeed. However, one could even carry it further and allow for "generics" on the type level (although, probably, that is much more complex on the compiler side):

module generic_module
  implicit none

  public :: template_type<T,R>


  type :: template_type<T, R>
    generics, type :: T
    generics, rank :: R
    <T>, dimension(<R>), allocatable :: content
  end type template_type<T, R>

end module generic_module


program test_generics
  use generic_module, my_type_int0 => template_type<integer, 0>,&
      & my_type_int1 => template_type<integer, 1>
  implicit none

  type(my_type_int0) :: i0
  type(my_type_int1) :: i1

  !...

end program test_generics

@cmacmackin
Copy link

I've been thinking a bit about the Fortran type system. In a language like C++, a variable's type encapsulates the sort of data it stores (int, uint, string, etc., with each of these corresponding to a unique kind), whether it's a pointer, whether it's an array, etc. In Fortran, there are really four different aspects to the type system, each of which is somewhat independent:

  • type (integer, real, derived type, etc): either known at compile time or deferred until run-time using polymorphism
  • kind (how a particular type is stored in memory): must be known at compile time
  • rank (number of dimensions of an array): until recently had to be known at compile time, but now can be deferred to run-time using the assumed-rank feature. Unlike type and len, however, it can not be dynamically allocated at run-time, making it more like old-fashioned assumed-size arrays rather than modern assumed-shape arrays
  • len/size (number of elements in each dimension of an array): can be deferred to run-time

Parameterised derived types take care of kind and len, where the latter can be deferred until run-time but the former must be known at compile-time. That severely limits the usefulness of parameterising the kinds in a type, because every combination of kind values requires its own implementation of a procedure which takes that type as an argument. And just to make it all the more confusing, unlimited polymorphic variables are able to take on any kind that the user wants, despite polymorphism really being about the type and not the kind.

Templates/generic programming in a language like C++ are conceptually fairly straightforward. While the code is written in such a way that it does not state the exact type (which, as we said, contains the information on the kind, rank, len) of the data it is operating on, all of this information will be available at compile-time. I don't think anyone has adequately considered how templates in Fortran should deal with the fact that there are really (up to) four pieces of information they need to know about, rather than one like in C++. Worse, the way these pieces of information are currently handled by the compiler are not really consistent, with some needing to be known at compile-time and others put off until run-time under certain circumstances.

My feeling is that we're going to need to step back somewhat if we are to consider how best to make generic programming work within the type-system as a whole. Perhaps we should think about introducing "assumed-kind" variables and a select kind construct. This could be modelled after the assumed-rank feature, with similar restrictions in place. (More ambitiously, we could think about making both rank and kind dynamically allocatable, I suppose, although I don't think that would be necessary for what I'm thinking about here). There need not be any additional run-time overhead, as the compiler could generate all necessary combinations of kind-parameters for a type or procedure at compile-time.

Let's say that derived types could then be parameterised on rank and that len parameters could then be a 1-D array:

type :: foo(r, s)
  integer, rank :: r = 2
  integer, dimension(r), len :: s = [3, 5]
  real, dimension(s) :: bar
end type foo

(Note that it remains unclear as to exactly how we'd handle allocatable/pointer arrays of parameterized rank, in that case.)

With those changes we could eliminate some of the inconsistency within the Fortran type system. At that point it starts to become easier to think about how we do the same for the type itself through templating. I think parameterised modules (with the inclusion of some sort of type parameter), the addition of a type-parameter to derived types, and/or (potentially parameterised) generic types described above might be able to handle it.

@aradi
Copy link
Contributor

aradi commented Oct 21, 2019

Could you make a demonstration, how the swap-module above would look then alike? I think, swap is a pretty good example, as it covers many aspects of the generics while being super simple.

@cmacmackin
Copy link

My thoughts aren't quite concrete enough for that yet. I'll try to come up with some examples of what we could do though.

@aradi
Copy link
Contributor

aradi commented Oct 21, 2019

Cool! I just have realized, that issue #4 has a very similar subject, maybe it would be worth to close this issue and continue discussion there?

@rweed
Copy link

rweed commented Oct 21, 2019

First thanks a big thanks to Ondrej and Zach for standing up this site. Its something some have advocated for a while and is long overdue. Also, thanks to Arjen. Your proposal looks like it might be a workable first step to templates and to me fits into current Fortran syntax etc. better than some of the other proposals I've seen. One idea that i had as a first step to a generic container was an extension of parameterized types to allow the syntax introduced in F2008 that allowed the type statement to define intrinisic variables to be allowed in the definition of the PDT but with an associated modification to the underlying KIND parameter facility to make it truly generic (syntax wise). Specifically, in F2008 you can do the following to define a double precision variable

USE ISO_FORTRAN_ENV
Type(REAL(REAL64)) :: areal

Ideally you would want to be able to just write

Type(REAL64) :: areal

Unfortunately, because the standard does not mandate that KIND parameters be unique in the sense that the values returned by the SELECTED_REAL, SELECTED_INT, and KIND functions along with the intrinsic parameters in ISO_FORTRAN_ENV are never the same value. Currently I think the majority of compilers will return 4 for both KIND(1) and KIND(1.0) plus INT64 and REAL64 are both 8. If the values were unique then for me parameterized types become a little more generic because I can do the following

Type :: genval(listkind)
Integer(kind) :: listkind
Type(listkind) :: aval
End Type

without having to create a separate type for each intrinsic value.
The only reason I can see that the current KIND facility doesn't mandate unique values is to placate people who still (foolishly) insist on doing

Real(8) :: areal
Integer(8) :: aint

However, I can't see any reason that the values returned by the various KIND setting functions and the intrinsic values in ISO_FORTAN_ENV can't be unique and still support the old Real(8) syntax. For the functions etc. just let the kind values returned for each intrinsic type live in a predefined range (ie. integers live in 100-199, reals in 200-299 etc).

These modifications would help to address support for containers with intrinsic values but still leave open how you support user defined types in a similar manner. I would like to hear others ideas on how the compiler might do this. And has been pointed out, you still have the len parameter issue that prevents PDT's from being statically polymorphic which is what templates are really all about.

@arjenmarkus
Copy link
Author

Thanks for these comments. I am going to study this conversation in more detail. Hope to reformulate my proposal in a couple of days.

@aradi
Copy link
Contributor

aradi commented Oct 21, 2019

@rweed Would it be possible to present a simple self containing example using your generalisation idea? For example, how would the swap-algorithm above look like when using your suggestion? (As stated above, the swap-algorithm contains many aspects of generic programming, while being very-simple.)

I think whatever we come up with, it should work with intrinsic types, derived types (maybe even with parameterized ones) and classes as well, each of those with arbitrary ranks. Do you think, your approach can cover those cases?

@certik
Copy link
Member

certik commented Oct 21, 2019

@arjenmarkus thanks for opening an issue for this. I posted here how to get involved with @tclune who is leading the effort:

#4 (comment)

@rweed
Copy link

rweed commented Oct 22, 2019

@aradi requested an example of what I'm proposing for modifying PDTs. First look at the following code that uses PDTs to define a node and list type for a circular-doubly-linked list of integers. This code compiles without errors with Intel 2019.5 ifort

Program testpdt

USE ISO_FORTRAN_ENV

Implicit NONE

Type :: CDLnode_t(ikind)
Integer, kind :: ikind
Type(CDLnode_t(ikind)), Pointer :: next_p => NULL()
Type(CDLnode_t(ikind)), Pointer :: prev_p => NULL()
Integer(KIND=ikind) :: ival
End Type

Type :: CDLlist_t(ikind)
Integer, kind :: ikind
Type(CDLnode_t(ikind)), Pointer :: head_p => NULL()
Integer(INT64) :: counter = 0_INT64
End Type
End Program testpdt

Now assume that the modification to KINDS I'm proposing is in place. CDLnode_t now becomes

Type :: CDLnode_t(ikind)
Integer, kind :: ikind
Type(CDLnode_t(ikind)), Pointer :: next_p => NULL()
Type(CDLnode_t(ikind)), Pointer :: prev_p => NULL()
Type(KIND=ikind) :: aval
End Type

This gives you an truly generic container for reals and ints. Support for character strings might take a little more thought. There is support for deferred length strings etc for PDTs but how to fit that into a truly generic framework is something we can discuss. The big issue is how do you support user defined types using just the kind parameters. There are obviously a lot of other issues that need to be addressed but I think what I'm proposing is a tiny first step that builds on current Fortran syntax and gives programmers another option besides using unlimited polymorphic variables (I've done this for lists, trees, hashmaps etc and its not pretty) or relying on preprocessor tricks to emulate templates.

@cmacmackin
Copy link

@tclune, in issue #30 you mentioned that "rank-agnostic array references" are likely to emerge in the 202X standard. This would likely be an important enabling feature for generic programming/templating, as it would allow the same code to work on any rank of array. I've been struggling a bit to think of decent syntax for generic rank variables (beyond what already exists for assumed-rank arrays, which aren't doing quite what we're looking for here given that their rank remains unknown at run-time). It might be useful if you could elaborate on what is being discussed for this new feature, as it could stimulate some ideas on this issue.

@certik
Copy link
Member

certik commented Oct 22, 2019

Let's create a separate issue for "rank-agnostic array references" to track that feature (link to committee papers/proposals, etc.).

@haraldkl
Copy link

I think Ada has a quite sophisticated generics system. I'd love it if some those concepts could be adopted in Fortran. See https://en.wikibooks.org/wiki/Ada_Programming/Generics.

@vansnyder
Copy link

I proposed parameterized modules in 2004. The paper was 04-383r1. It's similar in spirit to Arjen's proposal, but uses the MODULE and USE statements. It provides what Magne Haveraaen asked for in Tokyo.

04-383r1.pdf

@urbanjost
Copy link

For reference, I have been experimenting with templating using simple
string substitution in a Fortran preprocessor, and was considering
refactoring it to allow something like the following...

The vast majority of times I use templating it is for a generic interface.

Essentially, a "module procedure" declaration could "call" a generic
procedure from the interface block with a list of tokens.

It would not by itself handle when conditionals are required but I
already had fpp(1)-like conditional directives. Even without that, a
subset of problems that would lend themselves to conditional processing
could just be handled with more complex strings (like making a token
with values like 'character(len=:,kind=default)' and 'type(mytype)'
instead of simple words).

Supporting this only in a module would allow for the strings to be
specified in other places, like a PUBLIC directive instead of only
allowing it in an interface block, but then name mangling would be harder
to automate, but maybe something like

public pubname=>name(tokens ...)

would work as well.

interface swap
   module procedure swap('REAL','REAL32')
   module procedure swap(type='REAL',kind='REAL64')
   module procedure swap('INTEGER','INT8')
   module procedure swap('INTEGER','INT16')
   module procedure swap('INTEGER','INT32')
   module procedure swap('INTEGER','INT64')
   module procedure swap(type='logical',)
end interface

contains

! adding the TOKENS field indicates this procedure is
! only to be processed if referenced from an interface
! definition. Having a default specified by NAME='string'
! would be not just convenient, but document what strings 
! are expected and allow for them to be called by name.

elemental subroutine swap(x,y) TOKENS(TYPE='integer',KIND='int32')  

!@(#) M_sort::d_swap(3fp): swap two double variables

integer, parameter                 :: wp={KIND}
{TYPE}(kind={KIND}), intent(inout) :: x,y
{TYPE}(kind={KIND})                :: temp
{TYPE}(kind={KIND})                :: unused
   temp = x; x = y; y = temp

   ! note an issue with using _wp here :
   unused=10_wp  
   unused=10.0_wp  

end subroutine swap

If you allowed arrays to create permutations something like

   module procedure swap('INTEGER',['INT8','INT32','INT16','INT64'])

would be nice, and some way to conditionally build only if a {type,kind}
is supported, so if a compiler does not support REAL128 it could be
skipped, or all supported kinds could be detected and used.

So that got me re-reading some of the links regarding templating.

I was surprised how similiar some of the proposals were in many ways,
and how far they dated back at first, except for syntax details; but
the real question to me then became whether the Fortran language itself
should have templating syntax or if Fortran needs a Standard-specified
pre-processor (anyway) and that templating should be included there
instead of in the language.

I am wondering what the benefits are to placing templating straight into
the language versus Fortran developing a standard pre-processor that would
include templating?

For example, perhaps templating might occur at run-time, or only include
required instances of the routine versus a preprocessor generating a
pre-defined set? Is this seen as something that will basically be handled
by pre-processing the code by the compiler, or something accessible at
load/link or run time?

@vansnyder
Copy link

vansnyder commented May 8, 2022 via email

@urbanjost
Copy link

urbanjost commented May 9, 2022

Thanks. Not totally convinced a standardized preprocessor could not be tightly bound with the processor and provide the same checks, but then it essentially might as well be part of the language, I suppose. Thanks for the clear explanation. Indeed, I can use a preprocessor on a non-Fortran program and certainly generate incorrect code with the preprocessor being completely unaware of that; but it also allows applying information conditionally using information the templating proposals do not include, such as conditional coding depending on system or processor type, and so on. So the need for a preprocessor is reduced but not eliminated. For templating alone that is a big advantage though. Since I use preprocessing for other reasons (generating documentation, conditional code selection, recording date and time of compilation, .... it seemed like "well, let it do templating too, and keep the core language simpler"; but your explanation drives home a major advantage for the current approaches. Thanks again, I really enjoyed that explanation.

@klausler
Copy link

klausler commented May 9, 2022

One of the goals of the current design effort is that there will be no syntax or semantic errors within an instantiated paramaterized module or template if there are no syntax or semantic errors in the parameterized module or template. This is enforced with adequate declarations of the parameters and their relationships, checked by the processor before instantiation, which is not possible with a string-substitution, token-substitution, or macro system.

If this is a goal, it is not possible to completely achieve it. Not all errors stem from interactions between actual parameters to instantiations of parameterized entities. I appreciate a desire to have better error messages than those that even good C++ compliers emit for errors in the semantics of template instantiations, but to insist that all errors can and will be detected and diagnosed prior to instantiation is not a feasible design goal.

@certik
Copy link
Member

certik commented May 9, 2022

but to insist that all errors can and will be detected and diagnosed prior to instantiation is not a feasible design goal.

I can clarify the goals: the generics subgroup is currently going with templates with "restrictions" (called "strong concepts" in the C++ world). To understand the details what it means, you can read our comparison document here:

It explains exactly in what sense errors will be caught and when. In short, you can indeed catch all errors before instantiating, but obviously you can only do it at the instantiation site when you know the user types, and you check them against the "restrictions / strong concepts", and if they pass, then you can instantiate without errors. See the document above for the details, and the kinds of error messages you can expect (showing actual error messages in Rust, Haskell for strong concepts and C++ for weak concepts). Indeed, C++ cannot catch all errors prior to instantiation (it only supports "weak concepts"), but Haskell, Rust and Go catch all errors prior to instantiation ("strong concepts"), and that is the goal for Fortran also.

@klausler
Copy link

klausler commented May 9, 2022

Yes, thanks, I know about concepts, and have read the design.

Haskell doesn't really have concepts -- its typeclass constraints are really not the same thing. Standard ML's functors and signatures are a much closer match to the idea.

Fortran mandates that a compiler enforce hundreds of constraints at compilation time. I guarantee you that I can write a test program that violates at least one of them in an instantiation without violating any concept on a template parameter.

@certik
Copy link
Member

certik commented May 9, 2022

@klausler I think you are onto something. What you are saying is that if you write a templated function that has an argument a of type T, which is a template with restrictions (concepts), then you use a inside the function in various constructs that the compiler is required to check, then it will be very hard or impossible to design the restrictions parts in such a way to catch all these "checks" without an instantiation?

Ok, here is an example:

module swap_m
    implicit none
    private
    public :: swap_t

    template swap_t(T)
        private
        public :: swap

        type :: T
        end type

        interface swap
            module procedure swap_
        end interface
    contains
        subroutine swap_(x, y)
            type(T), intent(inout) :: x
            type(T) :: tmp

            tmp = x
            x = y
            y = tmp
        end subroutine
    end template
end module

Here the template T doesn't have any restrictions, in other words, you can't do much with it. The templated function swap assigns it (swaps it). Possibly the assignment operation should be specified in the restrictions for T. Can you show me an example of some more complicated function that uses T, that violates at least one of the Fortran constraints in an instantiation (without violating the "concept/requirement")?

@klausler
Copy link

klausler commented May 9, 2022

Sure. Write a template subprogram that abstracts the kind of a REAL argument. In the subprogram, use the kind to deduce the next kind of REAL with higher precision -- e.g., given default real, determine double precision -- and declare a local variable with that kind. You won't be able to instantiate this subprogram for the kind of REAL with the most precision available in the implementation, only for those with less than the maximum. Perhaps you can devise a way for the programmer to encode a restriction on the kind of the template REAL kind, but detecting a failure to impose such a restriction would require instantiation.

@certik
Copy link
Member

certik commented May 9, 2022

So something like this:

subroutine bad_swap(x, y)
type(T), intent(inout) :: x
type(T) :: tmp
real(kind(x)+4) :: r
...
tmp = x
x = y
y = tmp
end subroutine

The natural way in my mind how to make this work is that the compiler when compiling bad_swap to some intermediate representation would figure out the restriction on kind(x)+4 being a valid real kind (say either 4 or 8), and then it would require the user to specify this restriction (somehow) in the restriction for the template T. Whether this can be done in all cases I don't know, but these are the kinds of things the generics subgroup is trying to figure out.

@wyphan
Copy link

wyphan commented May 9, 2022 via email

@klausler
Copy link

klausler commented May 9, 2022

Obviously that's unportable as written, but the issue remains even when the code is written in a portable manner -- say selected_real_kind(p=precision(x)+1).

And don't focus on patching a single hole. The general problem is that the hundreds of compile-time constraints cannot all be checked without actually instantiating a templatized subprogram (or better, module, but those are infuriatingly excluded).

I think this would become more obvious if J3 were to require a demonstrable prototype implementation before standardizing the feature.

@certik
Copy link
Member

certik commented May 9, 2022

The general problem is that the hundreds of compile-time constraints cannot all be checked without actually instantiating a templatized subprogram.

I don't know if this is true. If it is true however, then we cannot do "strong concepts", we can only do "weak concepts", almost for the same reason C++ is doing "weak concepts". That would be quite a big change in direction, so we should have a solid answer to this.

Do you have another example of such constraints that would be hard to check without instantiating? Why can Rust and Go do it, but not Fortran? (Or Haskell, although you said it's slightly different there --- I don't know Haskell much).

It seems there are constraints on kind, type and rank to consider. You are right that there are hundreds of potential compile-time constraints that the compiler must check. It seems to me the compiler would simply check every statement/expression/declaration in a templatized function against the restrictions/concepts.

I think this would become more obvious if J3 were to require a demonstrable prototype implementation before standardizing the feature.

Yes! It should be an absolute requirement for all features. @everythingfunctional and I sat down two months ago in Santa Fe and started working on a prototype in LFortran (https://gitlab.com/lfortran/lfortran/-/merge_requests/1664), but I need to focus on compiling actual projects with LFortran before I can spend more time on this. However, you might be further along with Flang, so you should consider if you can create a prototype.

@klausler
Copy link

klausler commented May 9, 2022

Prototyping J3's designs is J3's job, not mine.

Constraints that are obvious concerns for a "no need to instantiate" design include C712, C714, C715, C718, C732-C734, C736, C737, C740, C748, C758, C764, C784, C785, C788, C791, C792, C798, C799, C7103, C7111, and that's just from a quick pass over subclause 7. Also, detecting ambiguous generic interfaces in templatized subprograms (or derived types in them) can't be done until the interfaces that depend on template arguments are instantiated.

@everythingfunctional
Copy link
Member

Prototyping J3's designs is J3's job, not mine.

Not that I disagree, but which compiler should they use for prototyping? And should they really be expected to do it without help from someone on that compiler's development team?

If this is a goal, it is not possible to completely achieve it.

Obviously the design of a single language feature can't prevent all possible bugs, but I think we can catch a lot of them. Catching any at time of processing a template is better than waiting till instantiation (IMO). No reason to suggest that if we can't be perfect there's no reason to do it at all.

And not that there's no use for trying to do something like this, but I think it might be better to not allow at first, until we can be more deliberate about it. For example, I think the template you'd want is something like the following

template foo(k)
  integer :: k
  require valid_but_not_largest_real_kind(k) ! We haven't even discussed this yet for value parameters
contains
  function bar(x) result(y)
    real(kind=k), intent(in) :: x
    real(kind=k) :: y

    integer, parameter :: wp = selected_real_kind(p=precision(x)+1)
    real(kind=wp) :: tmp
    ! now we can do the math with higher precision
  end function
end template

We haven't thought about how we could specify restrictions on the values of template parameters, but I think before we allow their use in a context where that value matters to whether the code will even compile, we should think about that.

@vansnyder
Copy link

vansnyder commented May 9, 2022 via email

@certik
Copy link
Member

certik commented May 9, 2022

Constraints that are obvious concerns for a "no need to instantiate" design include C712, C714, C715, C718, C732-C734, C736, C737, C740, C748, C758, C764, C784, C785, C788, C791, C792, C798, C799, C7103, C7111

Yes, all these would have to be checked.

Prototyping J3's designs is J3's job, not mine.

We are all volunteers on J3. If you wanted to help us out, I would really appreciate it. :)

@klausler
Copy link

klausler commented May 9, 2022

Constraints that are obvious concerns for a "no need to instantiate" design include C712, C714, C715, C718, C732-C734, C736, C737, C740, C748, C758, C764, C784, C785, C788, C791, C792, C798, C799, C7103, C7111

Yes, all these would have to be checked.

And each of them is a constraint for which I could write a template example that can't be checked prior to instantiation against a specific set of template arguments.

Prototyping J3's designs is J3's job, not mine.

We are all volunteers on J3. If you wanted to help us out, I would really appreciate it. :)

I did try with DO CONCURRENT and was met with nothing but denial that there's even a problem. No thanks.

@wyphan
Copy link

wyphan commented May 9, 2022

I did try with DO CONCURRENT

Not trying to hijack this thread, but I submitted a GSoC proposal for DO CONCURRENT offloading in GFortran so am genuinely curious. Is it this one? #62

@klausler
Copy link

klausler commented May 9, 2022

I did try with DO CONCURRENT

Not trying to hijack this thread, but I submitted a GSoC proposal for DO CONCURRENT offloading in GFortran so am genuinely curious. Is it this one? #62

Yes. DO CONCURRENT admits non-parallelizable usage that can't be detected at compilation time, and there's no standard way to promise to the compiler than a given DO CONCURRENT construct is free of such usage.

@certik
Copy link
Member

certik commented May 10, 2022

Yes. DO CONCURRENT admits non-parallelizable usage that can't be detected at compilation time, and there's no standard way to promise to the compiler than a given DO CONCURRENT construct is free of such usage.

I think that definitely would be a problem problem. I think the way forward is to implement an extension (say in Flang) that allows the user to promise to the compiler that a given construct is parallelizable, and then when we submit a paper with a prototype, I think we have a good chance to fix this.

@klausler
Copy link

Here are a couple examples of templatized Fortran that I believe demonstrate violations that would require instantiation to detect. These are obviously not tested and the syntax is almost certainly incorrect.

  template tmplt(ty)
    type :: ty; end type
  contains
    subroutine subr(x)
        type(ty), intent(in) :: x
        generic :: gen => inner1, inner2
        call gen(x) ! ambiguous if instantiated with ty = real
      contains
        subroutine inner1(x)
          type(ty), intent(in) :: x
        end subroutine
        subroutine inner2(x)
          real, intent(in) :: x
        end subroutine
    end subroutine
  end template
  template tmplt(ty)
    type :: ty; end type
   contains
    subroutine sub(x)
      type, extends(ty) :: ext ! error if ty is not a derived type
        real :: comp ! error if ty already has a component named "comp"
      end type
      type(ty) :: v
      v = ty() ! error if ty has type parameters or components without defaults
    end subroutine
  end template

@everythingfunctional
Copy link
Member

  template tmplt(ty)
    type :: ty; end type
   contains
    subroutine sub(x)
      type, extends(ty) :: ext ! error if ty is not a derived type
        real :: comp ! error if ty already has a component named "comp"
      end type
      type(ty) :: v
      v = ty() ! error if ty has type parameters or components without defaults
    end subroutine
  end template

I believe we have, or intend to, prohibit extending from template type parameters, at least for now. Also, we don't assume the accessibility of an intrinsic structure, let alone user defined, constructor for a template type parameter, so that usage isn't allowed either.

The other example is something I'm not sure we've considered very strongly yet. We may desire preventing use of template type parameters as arguments in generic interfaces with multiple actual procedures. Not quite sure if that has all the right implications. This is something worth thinking about.

@vansnyder
Copy link

vansnyder commented May 10, 2022 via email

@certik
Copy link
Member

certik commented May 10, 2022

Thanks Peter!

The first one is similar to a template specialization. I can see multiple ways forward:

  • Do not allow generic procedures if one of the arguments is a template; and possibly create some new separate syntax for template specialization
  • If the template ty = real, use the inner2 function, as in a template specialization

In the second example, the way I understand it, it would not compile, with an error message that you are using extends(ty) on a type ty that does not allow that. It is not written in the requirements part. To fix it, you have to "allow" such operations explicitly, something like this:

  template tmplt(ty)
    type :: ty; end type
    allow_extend(ty)
    type_params_and_components_have_defaults(ty)
   contains
    subroutine sub(x)
      type, extends(ty) :: ext ! allowed in the requirements section
        real :: comp ! -- multiple options here, see below
      end type
      type(ty) :: v
      v = ty() ! now works, as it is allowed in the requirements section
    end subroutine
  end template

The type :: ty; end type requirements says that there is a generic type ty, but you can't do much with it. You have to explicitly allow all operations that you want to do with it. I showed above how it could be done for the main two operations. Regarding the comp component, I know there was some discussion about components of templates and I can't remember right now if they decided to allow them or not. If it is not allowed, then you simply get a compiler error above. If it is allowed, then one approach is that you need to specify them in the parent template:

type :: ty
    real :: comp2
end type

That is the one and only component that is allowed (in the user type that will get passed in at call site), and it must be called comp2. Then in the extended type ext you know that comp2 is in the parent class and that comp is not there and thus you can declare it.

My understanding of the general approach is that initially you get compiler error for most of the usages above (and indeed you can always determine ahead of time if a given user type can be instantiated or not). Then we can add more ways to specify "requirements", and then you can use the template in more cases, such as your examples above, while keeping the property that if the user type satisfies the requirements, then it can always be instantiated.

@klausler
Copy link

Even if the goal of preventing any constraint violations in an instance of a template cannot be achieved, eliminating the vast majority of possible situations using a parameterized module or template system is better than catching none using a string-substitution preprocessor or a built-in token-substitution macro system.

It's a shame about the parameterized modules not being followed up on, though. Even if modules were parameterizable only by other modules (not types or anything else), you'd have an effective and highly composable solution, and the implementation would be easy to prototype and demonstrate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Clause 7 Standard Clause 7: Types
Projects
None yet
Development

No branches or pull requests