Stage Hygiene for Template Haskell - #243
Conversation
- Fix typos - Add proposed TH for build and target macros
It's all fake syntax, but I think this is more better.
I loosten up and allow non-stage-0 unexposed modules after all, the alternative that had those is adjusted accordingly.
Always good to be precise, and the disabled extension is tricky
- Greatly expand effects and interactions - Move some observations from detailed design to there - Be less terse with Cabal admitting a formalism is still needed. When the formalism is had, those bullets can go to effects and interactions. - Be slightly less terse with inverted-default extension
Cover some interactions there, stemming from lighter depedencies and the equational reasoning of stages. Rearrange treatment of core way and naive core interpreter as they are purely optional and so move to that section. Also clarify that plain hi is fine for stage 1, but "fat" interface file (thanks @michaelpj for the tip!) are needed for stage -1. The GHC subsection now comes after that, so it can say less on the naive core interpreter, and more on the big picture. Capitalize "Core".
|
There are some good ideas in this proposal and I would like to work with you on getting this proposal into shape. At the moment the key ideas are a bit lost in the weeds so it would be good to distinguish the fundamental from the accidental. |
|
I have not thought deeply about the cross-compilation problem for TH, but this seems plausible. I'm broadly in support of something like this, as long as it doesn't make TH harder to use for those not worried about cross-compilation (unless there's a very compelling reason to burden us). The motivation section of the proposal is compelling to me. |
|
Do we need to expose the full complexity of stage offsets in user-visible syntax? This comes up in a couple of places:
However the other existing systems seem like they can get away with only exposing two phases to the user:
I think both of these correspond to exposing the -1 and 0 stages and no others, and they have the advantage of being relatively easy to describe, whereas I am worried that the full stage offsetting machinery will be hard for people to understand (I certainly found it hard to understand!). While it might make sense for the underlying system to know about phases in full generality, I'd be interested in how much expressiveness we would lose if we were to restrict the surface syntax (at least initially - we could add a fuller version later if we needed it). For example, I think it would make it impossible to do anything too fancy within a single package (component?), but this might be an acceptable price to pay. |
|
@mpickering and I have talked quite a lot already, and I would like to thank him for putting up with me stumbling through a rather large backlog of hitherto unexpressed ideas.
You are on the money. I am definitely imposing a burden, but I hope to justify it.
Ah but see https://docs.racket-lang.org/guide/phases.html. There is a
So do note that I restrict the user to stages -1, 0, and 1 in a single module. This correspond's to Racket's |
|
CC @nh2 I wrote up @mboes's point about easier static linking, which should interest you. Where I wanted a "typed OK, so with that I think I've included all the new ideas, and hopefully clarified the background with a appendix on cross compilation in general. There are still the TODO's around clarifying the Cabal changes + bikeshedding cli syntax, but, well, I think it's better to make sure the GHC parts / big picture before that. |
|
@Ericson2314 Since I was just pinged here I haven't read everything yet, but here is another idea in case that isn't included yet: I believe most TH is some very simple "pure" syntax generation, like generating lenses. "Pure" means it doesn't do any Thus it can always be run on the host in the interpreter; no need for external interpreters or any object code generation. Thus I propose: Let's make a "pure" variant of TH that isn't allowed to do any |
|
@nh2 Yes I am sympathetic to that, but to be clear I don't think it is a substitute for what is being proposed here. For example in the static linking use-case, that wouldn't help only build SOs when needed (for TH), and ensure they weren't part of the run-time closure (to use the Nix term). |
|
I think that @nh2 's idea of having a "pure" part of TH is an excellent, and relatively small, first step in the direction of this proposal. It could be done independently, and might gather a lot more support. And is in line with other current proposals about TH. This current proposal is so huge, breaking it down into small steps is likely the best way of actually making forward progress. |
|
I'm worried about the opposite. I think most people won't know what they are missing and declare that good enough, sapping support for this proposal. Besides the other benefits like build incrementality and static linking, saying pure TH is good enough is saying there is some subset of Haskell where the build and host coincide. But I'm interested long term in cross compiling to really exotic targets: circuits, embedded systems with no dynamic memory allocation, etc etc. I really rather rip off the bandaid and start with no assumption the build and host have anything in common. It's the correct programming model. |
Thanks! Co-authored-by: Facundo Domínguez <facundominguez@gmail.com>
|
This proposal seems somewhat dormant. But since it was linked in a recent Twitter discussion, I figured I’d still chime in. I mostly want to say this: please consult the Racket prior art for this. There are a handful of fairly tricky details here, but Racket gets all of them right and has successfully used a phased compilation model for almost two decades now. The canonical publication about this is Composable and compilable macros: you want it when?, which explains the core issues concisely and outlines a clear solution. I can say from personal experience that Racket’s model gets all of this overwhelmingly right. It handles everything I’ve ever thrown at it (which is a lot), it’s easy to use, and it is conceptually simple. What’s more, it has since been successfully extended with additional features that one could, in theory, imagine Haskell programmers finding useful as well, such as nested modules, discussed in Submodules in Racket: You Want it When, Again?. Racket’s phasing system is sophisticated enough to support local macro definitions (i.e. elimination of the GHC stage restriction), predictable compilation with arbitrary, user-defined, compile-time state, and full macro hygiene. Fully adopting all the features of Racket’s module system is out of scope for TH in the short term, but there is no reason to not adopt a common subset, as many of those features would be useful in Haskell as well. In particular, I would recommend making positive numbers “more compile-time” phases and negative ones “closer to runtime” phases, simply because the choice is wholly arbitrary and opposite conventions would be needlessly confusing. (Also, in practice, references to positive phases seem to occur much more often in Racket than negative ones.) |
|
Being like Racket is absolutely the goal here. The order of the integers was an unconscious holdover from Nixpkgs where I last did this sort of thing; I'm happy with flipping the order. This is mainly been dormant as we [mainly @hsyl20!] chip away at https://github.com/hsyl20/ghc-cross-compilation. The prereqs for this are less controversial, and after those are done I think this proposal will be easier to discuss because the marginal cost will be substantially less. |
|
My suggestion would be to start small, with the simplest, most conservative change that yields tangible benefits. For me, this would be something like For example {-# LANGUAGE StagedImports #-}
{-# LANGUAGE TemplateHaskell #-}
module MyModule where
import {-# META #-} Data.Aeson.TH
import {-# UNRESTRICTED #-} Data.Singletons.TH
import Control.Lens.TH
data Foo = ...
deriveJSON defaultOptions ''Foo -- This is legal
genSingletons [''Foo] -- This is too
foo = genSingletons -- This also
makeLenses ''Foo -- This is not
someOptions = defaultOptions -- Neither is thisPackages making use of This would be a major improvement for the purposes of ghcide. We usually don't need to compile code and can just get away with typechecking, but in the presence of any module using With |
|
@wz1000 I'm sympathetic to the use-case and urgency, but I don't think that will be substantially easier than the whole thing, as I think most of the work GHC will be wrangling the internally separate modules for each stage. |
Don't we just need to split the |
|
My plan was just to split the whole module in two, so mainly changes in driver but compiling each module stays more same. (Eventually we can split apart Rn and Tc again because the computed splices will be provided by the driver, but that comes later.) |
|
I think I agree with @wz1000 here that starting like he proposes is best and then we can adapt a more complicated schema later. The majority of the benefit for normal users is gained from his proposal. |
|
The spit modules are needed for cross, but not just to solve the incrementally issues. For this one I would keep that approach, but for @mpickering's I also agree we might be able to get away with something simpler. That's why I like the division between the two proposals. That said, I'm not sure a split module isn't in fact the easier. The current interleaving of TH and other things is a mess, and "just splitting Still, this can all be dealt with later. Main thing to do is start incrementally approving designs. We can see how all the driver projects fit together after. |
…rpreter`) As it stands, we also need changes to `entropy`, but I'll manage that separately. Hopefully by reviving haskell#78. Note that the custom setup is only used for `cabal-doctest`, so we really can just safely remove it. This is essentially the same issue as cdepillabout/pretty-simple#82. Seeing as all the TH is just for `makeLenses`, in an ideal world we really ought to just be able to run on the host: see ghc-proposals/ghc-proposals#243. The changes to the two `.hs` files are from dumping the TH splices (and manually (well, with _some_ help from HLS) adding a load of qualified imports). The splices can be found in `dist-newstyle` after adding this to `cabal.project`: ``` package wreq ghc-options: -ddump-splices -dth-dec-file ```
…rpreter`) As it stands, we also need changes to `entropy`, but I'll manage that separately. Hopefully by reviving haskell/entropy#20. Note that the custom setup is only used for `cabal-doctest`, so we really can just safely remove it. This is essentially the same issue as cdepillabout/pretty-simple#82. Seeing as all the TH is just for `makeLenses`, in an ideal world we really ought to just be able to run on the host: see ghc-proposals/ghc-proposals#243. The changes to the two `.hs` files are from dumping the TH splices (and manually (well, with _some_ help from HLS) adding a load of qualified imports). The splices can be found in `dist-newstyle` after adding this to `cabal.project`: ``` package wreq ghc-options: -ddump-splices -dth-dec-file ```
Fix and clarify two things @shlevy noticed. Thanks! Co-authored-by: Shea Levy <shea@shealevy.com>
| ------------ | ||
|
|
||
| *N.B. this proposal uses the dreaded Autoconf "build" "host" "target" terminology.* | ||
| *.See the appendix for what those mean.* |
There was a problem hiding this comment.
| *.See the appendix for what those mean.* | |
| *See the appendix for what those mean.* |
| ~~~~~~~~~~~~~~~~ | ||
|
|
||
| In the olden days, one couldn't use Template Haskell with cross compilation at all. | ||
| This was bad for uses wishing to cross compiler, of course, but also bad for everyone else. |
There was a problem hiding this comment.
| This was bad for uses wishing to cross compiler, of course, but also bad for everyone else. | |
| This was bad for users wishing to cross compile, of course, but also bad for everyone else. |
| -- Oh no, the external interpreter cannot find those files, | ||
| -- or create those processes! | ||
|
|
||
| Also, while same-OS cross can sometimes be fairly lightweight |
There was a problem hiding this comment.
| Also, while same-OS cross can sometimes be fairly lightweight | |
| Also, while same-OS cross compiling can sometimes be fairly lightweight |
| $(...) -- fill in with dumped splice, no eval needed | ||
|
|
||
| This became easier with the changes on `this branch of GHC <https://gitlab.haskell.org/obsidiansystems/ghc/-/tree/wip/abrar/splices-8.6.5>`_. | ||
| Still, this requires building every package twice, since we redo the entire compilation on both platforms, and worse doesn't work if a top-level splice is target specific. |
There was a problem hiding this comment.
| Still, this requires building every package twice, since we redo the entire compilation on both platforms, and worse doesn't work if a top-level splice is target specific. | |
| Still, this requires building every package twice, since we redo the entire compilation on both platforms, and worse, it doesn't work if a top-level splice is target specific. |
| $(iosBoilerplateHelper ''SomeIosFfiType) | ||
| -- ^ error! 'SomeIosFfiType' doesn't exist | ||
|
|
||
| Even if ``SomeIosFfiType`` doesn't have any iOS-only types in its definition, the generated code probably refers to ios-only identifiers:: |
There was a problem hiding this comment.
| Even if ``SomeIosFfiType`` doesn't have any iOS-only types in its definition, the generated code probably refers to ios-only identifiers:: | |
| Even if ``SomeIosFfiType`` doesn't have any iOS-only types in its definition, the generated code probably refers to iOS-only identifiers:: |
| Despite Make's early popularizing of call-by-need semantics (if you squint) at build system authors, they reverted to conventional imperative thinking for cross compilation and bootstrapping. | ||
| If you focus on the building, the work, and who does it, then the build platform is given undo importance: | ||
| Native is normal, cross is weird; ``if cross then ... else ...`` code abounds. | ||
| But try focusing on the needing, the "why", so the subject and object are switched if you imagine being a anthropomorphized dependency node and looking at your now-flipped edges and new adjacent nodes like I do! |
There was a problem hiding this comment.
| But try focusing on the needing, the "why", so the subject and object are switched if you imagine being a anthropomorphized dependency node and looking at your now-flipped edges and new adjacent nodes like I do! | |
| But try focusing on the needing, the "why", so the subject and object are switched if you imagine being an anthropomorphized dependency node and looking at your now-flipped edges and new adjacent nodes like I do! |
|
|
||
| This is a rather colorful appendix, but I really hope to convey a shift in perspective that the dry definitions alone may fail to do. | ||
| Despite Make's early popularizing of call-by-need semantics (if you squint) at build system authors, they reverted to conventional imperative thinking for cross compilation and bootstrapping. | ||
| If you focus on the building, the work, and who does it, then the build platform is given undo importance: |
There was a problem hiding this comment.
| If you focus on the building, the work, and who does it, then the build platform is given undo importance: | |
| If you focus on the building, the work, and who does it, then the build platform is given undue importance: |
| Native is normal, cross is weird; ``if cross then ... else ...`` code abounds. | ||
| But try focusing on the needing, the "why", so the subject and object are switched if you imagine being a anthropomorphized dependency node and looking at your now-flipped edges and new adjacent nodes like I do! | ||
| The host platform regains its rightful primacy: | ||
| We "need" something to be built that "run" on our platform, we don't care where it is built. |
There was a problem hiding this comment.
| We "need" something to be built that "run" on our platform, we don't care where it is built. | |
| We "need" something to be built that "runs" on our platform, we don't care where it is built. |
|
|
||
| I said target "'target' almost isn't needed" because ironically, given my general disdain for abstract platforms other than build and host, this proposal makes them relevant again. | ||
| While stages less than 0, corresponding to "build", "pre-build", "pre-pre-build", etc, should never leak in the library's interface, | ||
| stages greater than 0---quotes and nested quotes---corresponding to "target, "post-target", "post-post-target", do and must influence the interface. |
There was a problem hiding this comment.
| stages greater than 0---quotes and nested quotes---corresponding to "target, "post-target", "post-post-target", do and must influence the interface. | |
| stages greater than 0 --- quotes and nested quotes --- corresponding to "target, "post-target", "post-post-target", do and must influence the interface. |
| stages greater than 0---quotes and nested quotes---corresponding to "target, "post-target", "post-post-target", do and must influence the interface. | ||
| From the building perspective this is simple: quotes are not eliminated by compilation, they remain in the interface, the package is thinking further and further ahead on how it is used. | ||
| From the needing perspective, things are more subtle. | ||
| If I have a ``-1`` import on ``Foo``, and splice some code from ``Foo``, I could end up with a quote form ``Foo`` in exposed in stage 0 of myself. |
There was a problem hiding this comment.
| If I have a ``-1`` import on ``Foo``, and splice some code from ``Foo``, I could end up with a quote form ``Foo`` in exposed in stage 0 of myself. | |
| If I have a ``-1`` import on ``Foo``, and splice some code from ``Foo``, I could end up with a quote from ``Foo`` exposed in stage 0 of myself. |
|
This is strictly speaking out of scope for TH, but dependent types does break this model a bit. On the one hand, values in type semantically need to match the target, on the other hand the typechecker needs to evaluate them on the host (or, more precisely, in the internal interpreter, if we have some kind of bytecode for the typechecker rather than using the results of native codegen). I raise it here only because a proper solution might involve changes relevant to just the TH case. Thanks to @goldfirere and @lexi-lambda for helping clarify the constraints here. |
Actually I haven't looked in detail but perhaps this is basically the same problem with typed template haskell? |
We do. Anything that is "transparent" to DH must have an unfolding. and then the type checker can munch on that. If you use data kinds and export a type synnonym the moral equivalent of that is happening. As long as there no effects at the type level, we should be fine.
Yeah I don't expect typed Template Haskell to work too well. It doesn't have an IMO good enough phase separation. I think https://davidchristiansen.dk/pubs/tyde2020-predictable-macros-abstract.pdf is better, giving regular template Haskell enough power that we can not miss typed template haskell. |
|
Excitedly closing in favor of #682! The cabal parts would become an https://github.com/haskell/ecosystem-proposals |
Template Haskell currently doesn't work well with cross compilation. This is a big nuisance for anyone making phone or browser software. To fix this, enforce stronger separation between the stages.
There are some TODOs, but I am purposefully leaving them there now to be resolved during discussion.
Rendered