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

Add TH TopEntityGeneration #729

Closed
wants to merge 19 commits into from
Closed

Conversation

blaxill
Copy link
Collaborator

@blaxill blaxill commented Aug 21, 2019

This is a work in progress to add TH functions to generate Synthesize annotations from Clash's NamedTypes annotations. It's based on an initial implementation by @thoughtpolice here and here. The difference with this version is that it will recursively attempt to find annotations in data types.

The motivation for me is that top entities with a large number of ports require haskell type + synthesize annotation which are verbose and can get out of sync.

Some drawbacks of this in its current state:

  • Messy implementation (If there is interest in this PR I will work to improve it) Improved somewhat
  • Extra dependency: I've used the recursion-schemes package simply as it was the method I'm most familiar with
  • Testing is brittle: I don't know of a better way to test the generated template haskell (see test file)
  • Incorrect generation when used incorrectly: This is the same as manually written Synthesize annotations, but we have the possibility to do better as we have access to the types.

An example:

data Simple = Simple ("simple1" ::: Int) ("simple2" ::: Bool)
data Named
  = Named
  { name1 :: "named1" ::: BitVector 3
  , name2 :: "named2" ::: BitVector 5
  }
data Embedded
  = Embedded
  { name3 :: "embedded1" ::: Simple
  , name4 :: "embedded2" ::: Bool
  }

topEntity :: "int"      ::: Signal System Int
           -> "tuple"    ::: ("tup1" ::: Signal System (BitVector 7), "tup2" ::: Signal System (BitVector 9))
           -> "simple"   ::: Signal System Simple
           -> "named"    ::: Signal System Named
           -> "embedded" ::: Signal System Embedded
           -> "out"      ::: Signal System Bool
topEntity = undefined
makeTopEntity 'topEntity
--   ======>
--     {-# ANN topEntity ((Synthesize "topEntity")
--                           [PortName "int",
--                            (PortProduct "tuple") [PortName "tup1", PortName "tup2"],
--                            (PortProduct "simple") [PortName "simple1", PortName "simple2"],
--                            (PortProduct "named") [PortName "named1", PortName "named2"],
--                            (PortProduct "embedded")
--                              [(PortProduct "embedded1")
--                                 [PortName "simple1", PortName "simple2"],
--                               PortName "embedded2"]])
--                          (PortName "out") #-}

Let me know what you think/any ideas to make this better

@blaxill
Copy link
Collaborator Author

blaxill commented Aug 21, 2019

It's likely the tests I've written is also version specific due to hardcoding template haskell splices, but I'm not sure on a better way?

@leonschoorl
Copy link
Member

If you split out the code that generates the TopEntity into a function that generates that as a TH expression (createTopEntity :: Name -> Maybe String -> ExpQ, or maybe even :: .. -> TExpQ TopEntity)
Then in the test you can splice in the TopEntity as a term and compare it with some expected TopEntity written manually.

The test could be something like:

expectedTopEntity1 =
  Synthesize "topEntity1")
    [PortName "in1", PortName "in2"])
    (PortName "out")

tests :: TestTree
tests =
  testGroup
    "topEntityGen"
    [ testCase "topEntity1" $
      $(createTopEntity 'topEntity1 Nothing) @?= expectedTopEntity1
    , ...
    ]

Much more readable and the test is TH version independent.

@blaxill
Copy link
Collaborator Author

blaxill commented Aug 21, 2019

Makes sense! I'll get to it in a day or two.

@blaxill
Copy link
Collaborator Author

blaxill commented Aug 26, 2019

hey @leonschoorl, any other thoughts on this ?

@thoughtpolice
Copy link
Contributor

Thanks for this, it's wonderful and much closer to what I wanted to (eventually) build!

Here's a bikeshed: what about the module name? There is some precedence for naming things .TH, e.g. lens puts all TH helpers under one module, and there is already a .TH module for Clash.Promoted.Nat.TH. So maybe we should just call it Clash.Annotations.TH or something, which could contain future APIs as well.

For example, I could see this same basic design being used for an implementation of e.g. #248 if (when) I implement it, so you could easily write blackbox modules and have all the crap auto-generated from the declaration you specify, just like here. In that case, we could also add a makeBlackBox to Clash.Annotations.TH as well.

@blaxill blaxill changed the title WIP: Add TH TopEntityGen Add TH TopEntityGeneration Sep 1, 2019
@blaxill
Copy link
Collaborator Author

blaxill commented Sep 1, 2019

hey @martijnbastiaan any thoughts on this PR ?

@martijnbastiaan
Copy link
Member

I like it! As the definition of ::: is

type (name :: k) ::: a = a

I wonder if we could use something like:

data AutoName

and

topEntity :: "int"      ::: Signal System Int
           -> "tuple"   ::: ("tup1" ::: Signal System (BitVector 7), "tup2" ::: Signal System (BitVector 9))
           -> "simple"  ::: Signal System Simple
           -> "named"   ::: Signal System Named
           -> AutoName  ::: Signal System Embedded
           -> "out"     ::: Signal System Bool

to autogenerate field names based on the record's field names. Anyway, that might be something to consider in the future. I'll give it a spin tomorrow and report back.

For the meantime, could you format you code according to the style guide? It's mostly:

buildPorts :: Type                                        -- Type to split at
           -> TypeF (Type, Q [PortName]) -> Q [PortName]  -- Fold step

to

buildPorts
  :: Type                                        
  -- ^ Type to split at
  -> TypeF (Type, Q [PortName]) -> Q [PortName]  
  -- ^ Fold step

Thanks!

@blaxill
Copy link
Collaborator Author

blaxill commented Sep 11, 2019

@martijnbastiaan I've made some style fixes and rebased. I'm not sure what the failure for 8.8.1 is. Any other changes you can think of for this PR?

@martijnbastiaan
Copy link
Member

@blaxill Looks good! I'd be happy to include it. I'll look into the GHC 8.8.1 error - I suspect it's another issue with version bounds on dependencies. I've rebased it again and will run it with our own CI. If that works I'll force push to this PR.

@christiaanb
Copy link
Member

@blaxill what happens if I don't annotate an argument or a result with a name, e.g

topEntity :: "int"      ::: Signal System Int
           -> "tuple"   ::: ("tup1" ::: Signal System (BitVector 7), "tup2" ::: Signal System (BitVector 9))
           -> "simple"  ::: Signal System Simple
           -> "named"   ::: Signal System Named
           -> Signal System Embedded
           -> "out"     ::: Signal System Bool

@@ -317,14 +319,16 @@ test-suite unittests
base,
hint >= 0.7 && < 0.10,
tasty >= 1.2 && < 1.3,
tasty-hunit
tasty-hunit ,
template-haskell >= 2.9.0.0 && < 2.14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be template-haskell >= 2.9.0.0 && < 2.16 to work with GHC-8.8

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you could just remove the bounds here and cabal will use the bounds places on the Library

@blaxill
Copy link
Collaborator Author

blaxill commented Sep 11, 2019

@christiaanb

topEntity :: "int"      ::: Signal System Int
           -> "tuple"   ::: ("tup1" ::: Signal System (BitVector 7), "tup2" ::: Signal System (BitVector 9))
           -> "simple"  ::: Signal System Simple
           -> "named"   ::: Signal System Named
           -> Signal System Embedded
           -> "out"     ::: Signal System Bool

That will fail with:

tests/Clash/Tests/TopEntityGeneration.hs:90:1: error:
    Failed to get a single name for argument 4
 a.k.a. AppT (AppT (ConT Clash.Signal.Internal.Signal) (ConT Clash.Signal.Internal.System)) (ConT Clash.Tests.TopEntityGeneration.Embedded)
 got name [PortProduct "embedded1" [PortName "simple1",PortName "simple2"],PortName "embedded2"]

Additionally:

topEntity :: "int"      ::: Signal System Int
           -> "tuple"   ::: ("tup1" ::: Signal System (BitVector 7), "tup2" ::: Signal System (BitVector 9))
           -> "simple"  ::: Signal System Simple
           -> "named"   ::: Signal System Named
           -> Signal System Int
           -> "out"     ::: Signal System Bool

Will also fail:

tests/Clash/Tests/TopEntityGeneration.hs:100:1: error:
    Failed to get a single name for argument 4
 a.k.a. AppT (AppT (ConT Clash.Signal.Internal.Signal) (ConT Clash.Signal.Internal.System)) (ConT GHC.Types.Int)
 got name []

Whereas this will work:

data SimpleNamed = SimpleNamed ("simpleNamed" ::: Int)
topEntity :: "int"      ::: Signal System Int
           -> "tuple"   ::: ("tup1" ::: Signal System (BitVector 7), "tup2" ::: Signal System (BitVector 9))
           -> "simple"  ::: Signal System Simple
           -> "named"   ::: Signal System Named
           -> Signal System SimpleNamed
           -> "out"     ::: Signal System Bool
  ======>
    {-# ANN topEntity Synthesize
                        {t_name = "topEntity",
                         t_inputs = [PortName "int",
                                     (PortProduct "tuple") [PortName "tup1", PortName "tup2"],
                                     (PortProduct "simple")
                                       [PortName "simple1", PortName "simple2"],
                                     (PortProduct "named") [PortName "named1", PortName "named2"],
                                     PortName "simpleNamed"],
                         t_output = PortName "out"} #-}

Some other failure cases might exist when using naming annotations on a subset of parameters in constructors. (E.g. note the PortProduct for "Ops" only gets one PortName child instead of two):

data Ops = Ops ("one":::Int) (Bool)

topEntity :: "int"      ::: Signal System Int
           -> "simple"  ::: (Signal System Ops)
           -> "out"     ::: Signal System Bool
makeTopEntity 'topEntity

  ======>
    {-# ANN topEntity Synthesize
                        {t_name = "topEntity",
                         t_inputs = [PortName "int",
                                     (PortProduct "simple") [PortName "one"]],
                         t_output = PortName "out"} #-}

I think it will work correctly if the unnamed child references a type that has annotation though.

I'll add a test case for your example. For my failure case its a bit more tricky and the function buildPorts would need to be extended to handle this.

@martijnbastiaan
Copy link
Member

I believe

a.k.a. AppT (AppT (ConT Clash.Signal.Internal.Signal) (ConT Clash.Signal.Internal.System)) (ConT Clash.Tests.TopEntityGeneration.Embedded)

would be more readable after a pprint (click).

@martijnbastiaan
Copy link
Member

Looking good @blaxill! I'll merge it after the CI is done testing.

@blaxill
Copy link
Collaborator Author

blaxill commented Sep 11, 2019

Looking good @blaxill! I'll merge it after the CI is done testing.

Hold on one second, I've just realised newtypes aren't supported, but it will only take me a second or two to add support and tests...

@martijnbastiaan
Copy link
Member

Np! CI won't be finished anytime soon I'm afraid..

@blaxill
Copy link
Collaborator Author

blaxill commented Sep 11, 2019

CI beat me to it :) I'm adding support for basic type/data families, eta tomorrow

Added support for:
- newtypes
- One step resolution of type/data family instances
- all constructor types
@blaxill
Copy link
Collaborator Author

blaxill commented Sep 12, 2019

Ok, this has support for one-step (non-recursive) type families, extra tests and better error handling. Assuming you don't want any changes from the latest commit this is good to go for me.

@martijnbastiaan
Copy link
Member

Haddock generation fails with the new patch. If I had to guess it would be:

'clash-prelude/tests/Clash/Tests/TopEntityGeneration.hs'

causing parse errors (as single quotes are usually used to reference to functions). I think replacing it with:

"Clash.Tests.TopEntityGeneration"

would be good enough.

@martijnbastiaan
Copy link
Member

@blaxill I've pushed it to our local CI too to speed it up. If you could give me a heads up when you think it's ready, that'd be great, then I'll squash & merge it.

@blaxill
Copy link
Collaborator Author

blaxill commented Sep 12, 2019

Ok, I've added all I can think of. Works locally for me, but I can't promise I've missed another incompatibility with differing base/template haskell versions until CI has finished.

Edit:

Looks like haddock still might be failing.

If you can skip CI on the previous pushes and only do the last that would be preferable. My intermediate commits have bogged down the CI sorry.

@martijnbastiaan
Copy link
Member

If you can skip CI on the previous pushes and only do the last that would be preferable. My intermediate commits have bogged down the CI sorry.

Done! We should be able to move everything to GitLab CI soon (we've been using it for our own branches/prs for a while now), which should improve the experience considerably.

@blaxill
Copy link
Collaborator Author

blaxill commented Sep 12, 2019

@martijnbastiaan Looks like its ready to go!

@martijnbastiaan
Copy link
Member

Great, I've squashed it in #795. Looks like the CI approved there as well. Thanks for your work @blaxill !

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

Successfully merging this pull request may close these issues.

5 participants