Why can't arguments be levity polymorphic for inline functions?

Hi All Not sure if this belongs in ghc-users or ghc-devs, but it seemed devy enough to put it here. Section 6.4.12.1 https://downloads.haskell.org/~ghc/9.0.1/docs/html/users_guide/exts/levity_p... of the GHC user manual points out, if we allowed levity polymorphic arguments, then we would have no way to compile these functions, because the code required for different levites is different. However, if such a function is {-# INLINE #-} or {-# INLINABLE #-} there's no need to compile it as it's full definition is in the interface file. Callers can just compile it themselves with the levity they require. Indeed callers of inline functions already compile their own versions even without levity polymorphism (for example, presumably inlining function calls that are known at compile time). The only sticking point to this that I could find was that GHC will only inline the function if it is fully applied https://downloads.haskell.org/ghc/9.0.1/docs/html/users_guide/exts/pragmas.h..., which suggests that the possibility of partial application means we can't inline and hence need a compiled version of the code. But this seems like a silly restriction, as we have the full RHS of the definition in the interface file. The caller can easily create and compile it's own partially applied version. It should be able to do this regardless of levity. It seems to me we're okay as long as the following three things aren't true simultaneously: 1. Blah has levity polymorphic arguments 2. Blah is exported 3. Blah is not inline If a function "Blah" is not exported, we shouldn't care about levity polymorphic arguments, because we have it's RHS on hand in the current module and compile it as appropriate. And if it's inline, we're exposing it's full RHS to other callers so we're still fine also. Only when these three conditions combine should we give an error, say like: "Blah has levity polymorphic arguments, is exported, and is not inline. Please either remove levity polymorphic arguments, not export it or add an {-# INLINE #-} or {-# INLINABLE #-} pragma. I presume however there are some added complications that I don't understand, and I'm very interested in what they are as I presume they'll be quite interesting. Thanks, Clinton

Hey Clinton, I think the state of things is best summarised it's in principle possible to implement. But it's unclear how best to do so or even if it's worth having this feature at all. The biggest issue being code bloat. As you say a caller could create it's own version of the function with the right kind of argument type. But that means duplicating the function for every use site (although some might be able to be commoned up). Potentially causing a lot of code bloat and compile time overhead. In a similar fashion we could create each potential version we need from the get go to avoid duplicating the same function. But that runs the risk of generating far more code than what is actually used. Last but not least GHC currently doesn't always load unfoldings. In particular if you compile a module with optimizations disabled the RHS is currently *not* available to the compiler when looking at the use site. There already is a mechanism to bypass this in GHC where a function *must* be inlined (compulsory unfoldings). But it's currently reserved for built in functions. We could just make INLINE bindings compulsory if they have levity-polymorphic arguments sure. But again it's not clear this is really desireable. I don't think this has to mean we couldn't change how things work to accomodate levity-polymorphic arguments. It just seems it's unclear what a good design would look like and if it's worth having. Cheers Andreas Am 08/10/2021 um 01:36 schrieb Clinton Mead:
Hi All
Not sure if this belongs in ghc-users or ghc-devs, but it seemed devy enough to put it here.
Section 6.4.12.1 https://downloads.haskell.org/~ghc/9.0.1/docs/html/users_guide/exts/levity_p... of the GHC user manual points out, if we allowed levity polymorphic arguments, then we would have no way to compile these functions, because the code required for different levites is different.
However, if such a function is {-# INLINE #-} or{-# INLINABLE #-} there's no need to compile it as it's full definition is in the interface file. Callers can just compile it themselves with the levity they require. Indeed callers of inline functions already compile their own versions even without levity polymorphism (for example, presumably inlining function calls that are known at compile time).
The only sticking point to this that I could find was that GHC will only inline the function if it is fully applied https://downloads.haskell.org/ghc/9.0.1/docs/html/users_guide/exts/pragmas.h..., which suggests that the possibility of partial application means we can't inline and hence need a compiled version of the code. But this seems like a silly restriction, as we have the full RHS of the definition in the interface file. The caller can easily create and compile it's own partially applied version. It should be able to do this regardless of levity.
It seems to me we're okay as long as the following three things aren't true simultaneously:
1. Blah has levity polymorphic arguments 2. Blah is exported 3. Blah is not inline
If a function "Blah" is not exported, we shouldn't care about levity polymorphic arguments, because we have it's RHS on hand in the current module and compile it as appropriate. And if it's inline, we're exposing it's full RHS to other callers so we're still fine also. Only when these three conditions combine should we give an error, say like:
"Blah has levity polymorphic arguments, is exported, and is not inline. Please either remove levity polymorphic arguments, not export it or add an {-# INLINE #-} or {-# INLINABLE #-} pragma.
I presume however there are some added complications that I don't understand, and I'm very interested in what they are as I presume they'll be quite interesting.
Thanks, Clinton
_______________________________________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs

Thanks for your reply Andreas.
Just some further thoughts, perhaps we don't even require inline.
Correct me if I'm wrong, but couldn't we compile even functions with levity
polymorphic arguments by just boxing all the arguments?
This would also mean the caller would have to box arguments before passing.
You'd also need to box any working variables inside the function with
levity other than `Type`.
This to a certain extent defeats the purpose of levity polymorphism when
it's intended as an optimisation to avoid boxed types, but it does give a
fallback we can always use.
Yes, if you inline you can get code bloat. But that's a risk when you
inline any function.
Cheers,
Clinton
On Fri, Oct 8, 2021 at 11:39 PM Andreas Klebinger
Hey Clinton,
I think the state of things is best summarised it's in principle possible to implement. But it's unclear how best to do so or even if it's worth having this feature at all.
The biggest issue being code bloat.
As you say a caller could create it's own version of the function with the right kind of argument type. But that means duplicating the function for every use site (although some might be able to be commoned up). Potentially causing a lot of code bloat and compile time overhead.
In a similar fashion we could create each potential version we need from the get go to avoid duplicating the same function. But that runs the risk of generating far more code than what is actually used.
Last but not least GHC currently doesn't always load unfoldings. In particular if you compile a module with optimizations disabled the RHS is currently *not* available to the compiler when looking at the use site. There already is a mechanism to bypass this in GHC where a function *must* be inlined (compulsory unfoldings). But it's currently reserved for built in functions. We could just make INLINE bindings compulsory if they have levity-polymorphic arguments sure. But again it's not clear this is really desireable.
I don't think this has to mean we couldn't change how things work to accomodate levity-polymorphic arguments. It just seems it's unclear what a good design would look like and if it's worth having.
Cheers Andreas Am 08/10/2021 um 01:36 schrieb Clinton Mead:
Hi All
Not sure if this belongs in ghc-users or ghc-devs, but it seemed devy enough to put it here.
Section 6.4.12.1 https://downloads.haskell.org/~ghc/9.0.1/docs/html/users_guide/exts/levity_p... of the GHC user manual points out, if we allowed levity polymorphic arguments, then we would have no way to compile these functions, because the code required for different levites is different.
However, if such a function is {-# INLINE #-} or {-# INLINABLE #-} there's no need to compile it as it's full definition is in the interface file. Callers can just compile it themselves with the levity they require. Indeed callers of inline functions already compile their own versions even without levity polymorphism (for example, presumably inlining function calls that are known at compile time).
The only sticking point to this that I could find was that GHC will only inline the function if it is fully applied https://downloads.haskell.org/ghc/9.0.1/docs/html/users_guide/exts/pragmas.h..., which suggests that the possibility of partial application means we can't inline and hence need a compiled version of the code. But this seems like a silly restriction, as we have the full RHS of the definition in the interface file. The caller can easily create and compile it's own partially applied version. It should be able to do this regardless of levity.
It seems to me we're okay as long as the following three things aren't true simultaneously:
1. Blah has levity polymorphic arguments 2. Blah is exported 3. Blah is not inline
If a function "Blah" is not exported, we shouldn't care about levity polymorphic arguments, because we have it's RHS on hand in the current module and compile it as appropriate. And if it's inline, we're exposing it's full RHS to other callers so we're still fine also. Only when these three conditions combine should we give an error, say like:
"Blah has levity polymorphic arguments, is exported, and is not inline. Please either remove levity polymorphic arguments, not export it or add an {-# INLINE #-} or {-# INLINABLE #-} pragma.
I presume however there are some added complications that I don't understand, and I'm very interested in what they are as I presume they'll be quite interesting.
Thanks, Clinton
_______________________________________________ ghc-devs mailing listghc-devs@haskell.orghttp://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs

One significant problem is that {-# INLINE #-} functions are not actually always inlined! Specifically, if an inline-function is not passed all of its arguments, it will not be inlined. This poses a problem for levity-polymorphic functions, and GHC already does some special handling of the few levity-polymorphic primitives, in case they are ever used without all of their arguments. As for boxing levity-polymorphic arguments: that's plausible, but then I think you've defeated the programmer's intended aim. The solution to me is some kind of so-called template polymorphism https://gitlab.haskell.org/ghc/ghc/-/issues/14917#note_151678. This might work, but it would take a fair amount of design work first. Richard
On Oct 7, 2021, at 7:36 PM, Clinton Mead
wrote: Hi All
Not sure if this belongs in ghc-users or ghc-devs, but it seemed devy enough to put it here.
Section 6.4.12.1 https://downloads.haskell.org/~ghc/9.0.1/docs/html/users_guide/exts/levity_p... of the GHC user manual points out, if we allowed levity polymorphic arguments, then we would have no way to compile these functions, because the code required for different levites is different.
However, if such a function is {-# INLINE #-} or {-# INLINABLE #-} there's no need to compile it as it's full definition is in the interface file. Callers can just compile it themselves with the levity they require. Indeed callers of inline functions already compile their own versions even without levity polymorphism (for example, presumably inlining function calls that are known at compile time).
The only sticking point to this that I could find was that GHC will only inline the function if it is fully applied https://downloads.haskell.org/ghc/9.0.1/docs/html/users_guide/exts/pragmas.h..., which suggests that the possibility of partial application means we can't inline and hence need a compiled version of the code. But this seems like a silly restriction, as we have the full RHS of the definition in the interface file. The caller can easily create and compile it's own partially applied version. It should be able to do this regardless of levity.
It seems to me we're okay as long as the following three things aren't true simultaneously:
1. Blah has levity polymorphic arguments 2. Blah is exported 3. Blah is not inline
If a function "Blah" is not exported, we shouldn't care about levity polymorphic arguments, because we have it's RHS on hand in the current module and compile it as appropriate. And if it's inline, we're exposing it's full RHS to other callers so we're still fine also. Only when these three conditions combine should we give an error, say like:
"Blah has levity polymorphic arguments, is exported, and is not inline. Please either remove levity polymorphic arguments, not export it or add an {-# INLINE #-} or {-# INLINABLE #-} pragma.
I presume however there are some added complications that I don't understand, and I'm very interested in what they are as I presume they'll be quite interesting.
Thanks, Clinton
_______________________________________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs

Clinton Mead
Hi All
Not sure if this belongs in ghc-users or ghc-devs, but it seemed devy enough to put it here.
Section 6.4.12.1 https://downloads.haskell.org/~ghc/9.0.1/docs/html/users_guide/exts/levity_p... of the GHC user manual points out, if we allowed levity polymorphic arguments, then we would have no way to compile these functions, because the code required for different levites is different.
However, if such a function is {-# INLINE #-} or {-# INLINABLE #-} there's no need to compile it as it's full definition is in the interface file. Callers can just compile it themselves with the levity they require. Indeed callers of inline functions already compile their own versions even without levity polymorphism (for example, presumably inlining function calls that are known at compile time).
The only sticking point to this that I could find was that GHC will only inline the function if it is fully applied https://downloads.haskell.org/ghc/9.0.1/docs/html/users_guide/exts/pragmas.h..., which suggests that the possibility of partial application means we can't inline and hence need a compiled version of the code. But this seems like a silly restriction, as we have the full RHS of the definition in the interface file. The caller can easily create and compile it's own partially applied version. It should be able to do this regardless of levity.
It seems to me we're okay as long as the following three things aren't true simultaneously:
1. Blah has levity polymorphic arguments 2. Blah is exported 3. Blah is not inline
In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon pragmas. This is a rather significant departure from the status quo, where one can remove all pragmas and still end up with a well-formed program. In this sense, pragmas aren't really part of the Haskell language but are rather bits of interesting metadata that the compiler may or may not pay heed to. Given that levity polymorphic functions have rather deep implications on compilation strategy, I suspect that the cleanest path to allowing it would be to further extend the type system (for instance, with a new "macro expanding" arrow notion). Cheers, - Ben

On Fri, Oct 8, 2021 at 10:51 AM Ben Gamari
In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon pragmas. This is a rather significant departure from the status quo, where one can remove all pragmas and still end up with a well-formed program. In this sense, pragmas aren't really part of the Haskell language but are rather bits of interesting metadata that the compiler may or may not pay heed to.
I don't believe this is really the status quo. In particular, the pragmas relating to overlapping instances definitely do affect whether a program type-checks or not.

Chris Smith
On Fri, Oct 8, 2021 at 10:51 AM Ben Gamari
wrote: In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon pragmas. This is a rather significant departure from the status quo, where one can remove all pragmas and still end up with a well-formed program. In this sense, pragmas aren't really part of the Haskell language but are rather bits of interesting metadata that the compiler may or may not pay heed to.
I don't believe this is really the status quo. In particular, the pragmas relating to overlapping instances definitely do affect whether a program type-checks or not.
Yes, this is a fair point. Moreover, the same can be said of LANGUAGE pragmas more generally. I will rephrase my statement to reflect what was in my head when I initially wrote it:
In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon INLINE pragmas. This is a rather significant departure from the status quo, where one can remove all INLINE, INLINEABLE, RULES, and SPECIALISE pragmas and still end up with a well-formed program.
These pragmas all share the property that they don't change program semantics but rather merely affect operational behavior. Consequently, they should not change whether a program should be accepted. Cheers, - Ben

Ben,
The suggestion of erroring if the inline pragma was not there was just
because I thought it would be better than silently doing something
different. But that's just a subjective opinion, it's not core to what I'm
proposing.
Indeed there are two other options:
1. Make levity polymorphic functions implicitly inline OR
2. Compile a version which wraps all the levity polymorphism in boxes.
Either approach would mean the program would still be accepted with or
without the pragma. Whether either of them are a good idea is debatable,
but it shows it's not necessary to require a pragma.
So if it's important that excluding a pragma doesn't result in a program
being rejected, either of the above options would solve that issue.
On Sat, Oct 9, 2021 at 2:06 AM Ben Gamari
Chris Smith
writes: On Fri, Oct 8, 2021 at 10:51 AM Ben Gamari
wrote: In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon pragmas. This is a rather significant departure from the status quo, where one can remove all pragmas and still end up with a well-formed program. In this sense, pragmas aren't really part of the Haskell language but are rather bits of interesting metadata that the compiler may or may not pay heed to.
I don't believe this is really the status quo. In particular, the pragmas relating to overlapping instances definitely do affect whether a program type-checks or not.
Yes, this is a fair point. Moreover, the same can be said of LANGUAGE pragmas more generally. I will rephrase my statement to reflect what was in my head when I initially wrote it:
In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon INLINE pragmas. This is a rather significant departure from the status quo, where one can remove all INLINE, INLINEABLE, RULES, and SPECIALISE pragmas and still end up with a well-formed program.
These pragmas all share the property that they don't change program semantics but rather merely affect operational behavior. Consequently, they should not change whether a program should be accepted.
Cheers,
- Ben

On Oct 8, 2021, at 10:51 AM, Ben Gamari
wrote: In my mind the fundamental problem with this approach is that it means that a program's acceptance by the compiler hinges upon pragmas.
I think being able to ignore pragmas is a worthy goal, but one we're rather far short of, at the moment: overlapping pragmas (as Chris has pointed out), language pragmas (!), options pragmas setting -Werror, plugin pragmas, maybe others. Richard

We do have a few such functions, and we give them a "compulsory unfolding" which means they MUST be inlined at EVERY call site. But
* Usually if a module exports a function, it generates code for that function. But for these guys it can't. We don't have a mechanism for *not* generating code for user-defined functions. We could add an INLINE-COMPULSORY pragma perhaps.
* Even then we'd have to check that every call of such a function is applied to enough arguments to get rid of all levity/representation polymorphism; so that when it is inlined all is good. It's not clear how to do that in general.
That's the kind of thing Richard means by "templates".
Simon
PS: I am leaving Microsoft at the end of November 2021, at which point simonpj@microsoft.commailto:simonpj@microsoft.com will cease to work. Use simon.peytonjones@gmail.commailto:simon.peytonjones@gmail.com instead. (For now, it just forwards to simonpj@microsoft.com.)
From: ghc-devs

Note that you can use Typed Template Haskell as a workaround, e.g.
f :: forall r (a :: TYPE r). Code Q (a -> a)
f = [||\x -> x||]
In the future this might be integrated better with the restriction
polymorphism checking:
https://gitlab.haskell.org/ghc/ghc/-/issues/18170
https://gitlab.haskell.org/ghc/ghc/-/wikis/FixedRuntimeRep#typed-template-ha...
On Fri, Oct 8, 2021 at 5:19 PM Simon Peyton Jones via ghc-devs
We do have a few such functions, and we give them a “compulsory unfolding” which means they MUST be inlined at EVERY call site. But
Usually if a module exports a function, it generates code for that function. But for these guys it can’t. We don’t have a mechanism for *not* generating code for user-defined functions. We could add an INLINE-COMPULSORY pragma perhaps. Even then we’d have to check that every call of such a function is applied to enough arguments to get rid of all levity/representation polymorphism; so that when it is inlined all is good. It’s not clear how to do that in general.
That’s the kind of thing Richard means by “templates”.
Simon
PS: I am leaving Microsoft at the end of November 2021, at which point simonpj@microsoft.com will cease to work. Use simon.peytonjones@gmail.com instead. (For now, it just forwards to simonpj@microsoft.com.)
From: ghc-devs
On Behalf Of Clinton Mead Sent: 08 October 2021 00:37 To: ghc-devs@haskell.org Subject: Why can't arguments be levity polymorphic for inline functions? Hi All
Not sure if this belongs in ghc-users or ghc-devs, but it seemed devy enough to put it here.
Section 6.4.12.1 of the GHC user manual points out, if we allowed levity polymorphic arguments, then we would have no way to compile these functions, because the code required for different levites is different.
However, if such a function is {-# INLINE #-} or {-# INLINABLE #-} there's no need to compile it as it's full definition is in the interface file. Callers can just compile it themselves with the levity they require. Indeed callers of inline functions already compile their own versions even without levity polymorphism (for example, presumably inlining function calls that are known at compile time).
The only sticking point to this that I could find was that GHC will only inline the function if it is fully applied, which suggests that the possibility of partial application means we can't inline and hence need a compiled version of the code. But this seems like a silly restriction, as we have the full RHS of the definition in the interface file. The caller can easily create and compile it's own partially applied version. It should be able to do this regardless of levity.
It seems to me we're okay as long as the following three things aren't true simultaneously:
1. Blah has levity polymorphic arguments
2. Blah is exported
3. Blah is not inline
If a function "Blah" is not exported, we shouldn't care about levity polymorphic arguments, because we have it's RHS on hand in the current module and compile it as appropriate. And if it's inline, we're exposing it's full RHS to other callers so we're still fine also. Only when these three conditions combine should we give an error, say like:
"Blah has levity polymorphic arguments, is exported, and is not inline. Please either remove levity polymorphic arguments, not export it or add an {-# INLINE #-} or {-# INLINABLE #-} pragma.
I presume however there are some added complications that I don't understand, and I'm very interested in what they are as I presume they'll be quite interesting.
Thanks, Clinton
_______________________________________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs
participants (7)
-
Andreas Klebinger
-
Ben Gamari
-
Chris Smith
-
Clinton Mead
-
Krzysztof Gogolewski
-
Richard Eisenberg
-
Simon Peyton Jones