Fallible unpacking#3177
Conversation
martijnbastiaan
left a comment
There was a problem hiding this comment.
@rslawson Not a real review yet, just wanted to drop by to say that it's awesome that you're working on this :). It seems very sensible to me to add this as a separate maybeUnpack function that's a member of BitPack.
|
|
||
| {-# INLINE maybeFromInteger_INLINE #-} | ||
| maybeFromInteger_INLINE :: forall n . (HasCallStack, KnownNat n) => Integer -> Maybe (Index n) | ||
| maybeFromInteger_INLINE i = bound `seq` if i > (-1) && i < bound then Just (I i) else Nothing |
There was a problem hiding this comment.
Entirely because I copied fromInteger_INLINE and just changed a couple things. @rowanG077 suggested when I talked with him in the office that I should probably rewrite that function in terms of this one, but I forgot to do that before committing this.
There was a problem hiding this comment.
The seq is there in fromInteger_INLINE and was copied here I guess. I'm not sure why it's there either. It was introduced in 8ca93af but the only thing the commit says is A more readable implementation of Index.fromInteger
| otherwiseNothing = [(NormalG (ConE 'True), ConE 'Nothing)] | ||
| unpackBody = MultiIfE (justMatches ++ otherwiseNothing) | ||
| unpackLambda = LamE [VarP argName] unpackBody | ||
| unpackApplied = (VarE 'dontApplyInHDL) `AppE` unpackLambda `AppE` (VarE (argNameIn)) |
There was a problem hiding this comment.
Any reason you're using magic like dontApplyInHDL? Just naively I would expect the functionality of this to be entirely implementable without it.
There was a problem hiding this comment.
It's there from copying over parts of the code in buildUnpack. I wasn't sure which parts I should or shouldn't keep, so I just went with the strategy of take what I can if it doesn't break things.
There was a problem hiding this comment.
The source of that dontApplyInHdl is this commit (part of this PR, the introduction of custom bit-representations).
While it does clean up the generated HDL, it is also the only occurrence I know of so far that requires BitPack to have the same bit representation as the type itself, causing errors if it is not.
a5598eb to
203dda6
Compare
|
To give an update on this: I want to do some tests with yosys locally to see what kind of resource usage we can expect from |
|
I tested bittide by replacing So it is a regression. Testing separately does show it does optimize things out correctly but it's probably not always possible in larger structures. For me this PR can now be merged. I will retag the reviewers. |
|
@rowanG077 Do you have a top entity you can share? I'll push it through Vivado. |
| unpack = checkUnpackUndef unpackChar# | ||
| pack = packXWith packChar# | ||
| unpack = checkUnpackUndef unpackChar# | ||
| maybeUnpack = Just . unpack |
There was a problem hiding this comment.
>>> unpack 0x110000 :: Char
*** Exception: Prelude.chr: bad argument: 1114112Unicode code points lie in the range U+0000 to U+D7FF and U+E000 to U+10FFFF. It seems Haskell isn't too concerned with the gap, it doesn't error for values in that range that I tried. But it does bottom with an ErrorCall for numbers larger than 0x10ffff. Not an XException, mind you.
I think it'd be better if maybeUnpack returned Nothing for out-of-range numbers.
There was a problem hiding this comment.
Values in the gap round-trip neatly in GHC 9.10.3:
>>> all (\x -> x == (ord $ chr x)) [0xd800 .. 0xdfff]
TrueThere was a problem hiding this comment.
Agree Peter! This is also one of the rules of NumConvert: succes -> not bottom. It makes a lot of sense to make conversion functions do this across the prelude.
|
Ah, this is a very welcome addition, thanks! I've only quickly scrolled through so far, I'll give it a bit more time later. |
af4aad7 to
797dfe6
Compare
|
@martijnbastiaan Yes The |
There was a problem hiding this comment.
Looking good! Aside from the true issue with maybeUnpack for Char I already commented on, I just wonder why a test is what it is, have a small suggestion regarding an error message and I wanted to show the reader what a piece of TH does, just as information.
| ] | ||
|
|
||
| in InstanceD Nothing context instTy [bitSizeType, pack, unpack] | ||
| maybeUnpack = |
There was a problem hiding this comment.
Hah, you go in thinking it'll just be very similar to unpack but with a Functor (Maybe _) like so many other instances, and it turns out to be a lot more complicated! Well, in TH. In Haskell it's still very basic, but still.
This definition looks fine, I was just wondering why it was more complicated than others and understand it now. The TH is still somewhat readable, but I asked GHC to read it for me so it becomes more readable. Just for reference, -ddump-splices tells us the three-tuple is:
instance (BitPack a,
KnownNat (BitSize a),
BitPack (a0, a1),
KnownNat (BitSize (a0, a1))) =>
BitPack (a, a0, a1) where
type BitSize (a, a0,
a1) = (+) (BitSize a) (BitSize (a0, a1))
pack tup
= pack (retup tup)
where
retup (a, a0, a1)
= (a, (a0, a1))
unpack x
= let
(a, y) = unpack x
(a0, a1) = unpack y
in (a, a0, a1)
maybeUnpack x
= let
bvL :: BitVector (BitSize a)
bvR :: BitVector (BitSize (a0, a1))
(bvL, bvR) = split# x
in
case (maybeUnpack bvL, maybeUnpack bvR) of
(Just a, Just (a0, a1))
-> Just (a, a0, a1)
_ -> NothingI've cleaned up the names for readability.
There was a problem hiding this comment.
I just started wondering why you give bvL and bvR type signatures. Surely from the fact that they unpack to a and (a0, a1), GHC should be able to imply where to split# the bit vector? I copied the code in the previous message in the place of deriveBitPackTuples, removed the type signatures of bvL and bvR and sure enough it compiles and works:
maybeUnpack x
= let
(bvL, bvR) = split# x
in
case (maybeUnpack bvL, maybeUnpack bvR) of
(Just a, Just (a0, a1))
-> Just (a, a0, a1)
_ -> Nothingso it's possible to simplify this TH AST a bit.
| maybeUnpack# :: forall n. KnownNat n => BitVector (CLogWZ 2 n 0) -> Maybe (Index n) | ||
| maybeUnpack# bv | ||
| | bound == 0 = Nothing | ||
| | bv <= maxBoundBV = Just (unpack# bv) |
There was a problem hiding this comment.
I think this is good enough, but it is a bit of a pity that the error message isn't as straight-forward as the unpack case:
>>> pack (maxBound :: Index 3)
0b10
>>> let bv = $(bLit "0.")
>>> unpack bv :: Index 3
*** Exception: X: Index.unpack called with (partially) undefined arguments: 0b0.
>>> maybeUnpack bv :: Maybe (Index 3)
*** Exception: X: Clash.Sized.BitVector.<= called with (partially) undefined arguments: 0b0. <= 0b10Do we want to improve the error message with something funky like
maybeUnpack# bv
| bound == 0 = Nothing
| !i <- unpack# bv
, bv <= maxBoundBV = Just i
| otherwise = Nothing
where
bound = natToInteger @n
maxBoundBV = pack# (maxBound# @n)?
That way maybeUnpack# still doesn't require a black box. The error message is still referencing Index.unpack though. We could also use clashSimulation to only dive into the BV constructor in simulation and use the code as it is now for HDL generation! That might be even better.
There was a problem hiding this comment.
Wait, I'm being silly, by forcing unpack# to WHNF before checking for out-of-bounds, it will bottom for out-of-bounds values through the check in fromInteger_INLINE.
But the following works in simulation and produces nice HDL (tested in GHC 9.12.4):
maybeUnpack# bv
| bound == 0 = Nothing
| clashSimulation
, BV mask _ <- bv
, mask > 0 = undefError "Index.maybeUnpack" [bv]
| bv <= maxBoundBV = Just (unpack# bv)
| otherwise = Nothing
where
bound = natToInteger @n
maxBoundBV = pack# (maxBound# @n)| assertNothing label (Just _) = | ||
| P.error (label P.<> ": expected Nothing, but got Just _") | ||
|
|
||
| mainCommon :: IO () |
There was a problem hiding this comment.
Doesn't this outputTest just do some stuff that can be done in clash-prelude/tests/Clash/Tests/BitPack.hs, and in fact it already overlaps with that? I think any non-overlap is better put in the Tasty suite over in clash-prelude.
Made in response to the discussion post at #3150
797dfe6 to
6e7a931
Compare
As per #3150, this is an implementation of fallible unpacking (
BitVector (BitSize a) -> Maybe a). This is largely separate from the existingunpackimplementations, except where those can be relied upon for easy implementations (maybeUnpack = Just . unpack).Still TODO: