Skip to content

Commit 14209c7

Browse files
Merge pull request #2451 from clash-lang/fix-2450
Expose non-`XException` bottoms in `hasX`
2 parents 95ee5b9 + c66cdfe commit 14209c7

File tree

8 files changed

+91
-31
lines changed

8 files changed

+91
-31
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CHANGED: `hasX` now needs an `NFDataX` constraint, in addition to an `NFData` one. This API change was made to fix an issue where `hasX` would hide error calls in certain situations, see [#2450](https://github.com/clash-lang/clash-compiler/issues/2450).

clash-prelude/clash-prelude.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ test-suite unittests
442442
Clash.Tests.TopEntityGeneration
443443
Clash.Tests.Unsigned
444444
Clash.Tests.Vector
445+
Clash.Tests.XException
445446

446447
Clash.Tests.Laws.Enum
447448
Clash.Tests.Laws.SaturatingNum

clash-prelude/src/Clash/XException.hs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Copyright : (C) 2016, University of Twente,
33
2017, QBayLogic, Google Inc.
44
2017-2019, Myrtle Software Ltd,
5-
2021-2022, QBayLogic B.V.
5+
2021-2023, QBayLogic B.V.
66
License : BSD2 (see the file LICENSE)
77
Maintainer : QBayLogic B.V. <[email protected]>
88
@@ -258,13 +258,16 @@ hwSeqX = seqX
258258
infixr 0 `hwSeqX`
259259

260260
-- | Evaluate a value with given function, returning 'Nothing' if it throws
261-
-- 'XException'.
262-
--
263-
-- > maybeX hasX 42 = Just 42
264-
-- > maybeX hasX (XException msg) = Nothing
265-
-- > maybeX hasX (3, XException msg) = Nothing
266-
-- > maybeX hasX (3, _|_) = _|_
267-
-- > maybeX hasX _|_ = _|_
261+
-- 'XException'. Note that non-'XException' errors take precedence over 'XException'
262+
-- ones
263+
--
264+
-- > maybeX hasX 42 = Just 42
265+
-- > maybeX hasX (XException msg) = Nothing
266+
-- > maybeX hasX (3, XException msg) = Nothing
267+
-- > maybeX hasX (XException msg, _|_) = _|_
268+
-- > maybeX hasX (_|_, XException msg) = _|_
269+
-- > maybeX hasX (3, _|_) = _|_
270+
-- > maybeX hasX _|_ = _|_
268271
-- >
269272
-- > maybeX isX 42 = Just 42
270273
-- > maybeX isX (XException msg) = Nothing
@@ -275,15 +278,18 @@ infixr 0 `hwSeqX`
275278
maybeX :: (a -> Either String a) -> a -> Maybe a
276279
maybeX f a = either (const Nothing) Just (f a)
277280

278-
-- | Fully evaluate a value, returning 'Nothing' if it throws 'XException'.
281+
-- | Fully evaluate a value, returning 'Nothing' if it throws 'XException'. Note
282+
-- that non-'XException' errors take precedence over 'XException' ones.
279283
--
280-
-- > maybeHasX 42 = Just 42
281-
-- > maybeHasX (XException msg) = Nothing
282-
-- > maybeHasX (3, XException msg) = Nothing
283-
-- > maybeHasX (3, _|_) = _|_
284-
-- > maybeHasX _|_ = _|_
284+
-- > maybeHasX 42 = Just 42
285+
-- > maybeHasX (XException msg) = Nothing
286+
-- > maybeHasX (3, XException msg) = Nothing
287+
-- > maybeHasX (XException msg, _|_) = _|_
288+
-- > maybeHasX (_|_, XException msg) = _|_
289+
-- > maybeHasX (3, _|_) = _|_
290+
-- > maybeHasX _|_ = _|_
285291
--
286-
maybeHasX :: NFData a => a -> Maybe a
292+
maybeHasX :: (NFData a, NFDataX a) => a -> Maybe a
287293
maybeHasX = maybeX hasX
288294

289295
-- | Evaluate a value to WHNF, returning 'Nothing' if it throws 'XException'.
@@ -300,20 +306,26 @@ maybeIsX = maybeX isX
300306
-- If you want to determine if a value contains undefined parts, use
301307
-- 'hasUndefined' instead.
302308
--
303-
-- > hasX 42 = Right 42
304-
-- > hasX (XException msg) = Left msg
305-
-- > hasX (3, XException msg) = Left msg
306-
-- > hasX (3, _|_) = _|_
307-
-- > hasX _|_ = _|_
309+
-- > hasX 42 = Right 42
310+
-- > hasX (XException msg) = Left msg
311+
-- > hasX (3, XException msg) = Left msg
312+
-- > hasX (XException msg, _|_) = _|_
313+
-- > hasX (_|_, XException msg) = _|_
314+
-- > hasX (3, _|_) = _|_
315+
-- > hasX _|_ = _|_
308316
--
309317
-- If a data structure contains multiple 'XException's, the "first" message is
310-
-- picked according to the implementation of 'rnf'.
311-
hasX :: NFData a => a -> Either String a
318+
-- picked according to the implementation of 'rnfX'.
319+
hasX :: (NFData a, NFDataX a) => a -> Either String a
312320
hasX a =
321+
-- TODO: Whenever 'a' contains an 'XException', we need to reevaluate the
322+
-- structure using 'rnfX' to make sure it didn't also contain another
323+
-- error call. We could prevent the two traversals by making 'hasX' a
324+
-- type class method. Also see: https://github.com/clash-lang/clash-compiler/issues/2450.
313325
unsafeDupablePerformIO
314326
(catch
315327
(evaluate (rnf a) >> return (Right a))
316-
(\(XException msg) -> return (Left msg)))
328+
(\(XException msg) -> evaluate (rnfX a) >> return (Left msg)))
317329
{-# NOINLINE hasX #-}
318330

319331
-- | Evaluate a value to WHNF, returning @'Left' msg@ if is a 'XException'.

clash-prelude/src/Clash/XException/MaybeX.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import Control.Applicative
3535
import Control.DeepSeq (NFData)
3636
import Control.Exception (throw)
3737

38-
import Clash.XException (XException(..), isX, hasX)
38+
import Clash.XException (XException(..), NFDataX, isX, hasX)
3939

4040
-- | Structure helping programmers to deal with 'Clash.XException.XException'
4141
-- values. For safety reasons it can't be constructed directly, but should be
@@ -83,7 +83,7 @@ toMaybeX = pure
8383

8484
-- | Construct a 'MaybeX' value. If 'hasX' evaluates to 'Left', this function
8585
-- will return 'IsX'. Otherwise, it will return 'IsDefined'.
86-
hasXToMaybeX :: NFData a => a -> MaybeX a
86+
hasXToMaybeX :: (NFDataX a, NFData a) => a -> MaybeX a
8787
hasXToMaybeX = either IsX_ IsDefined_ . hasX
8888

8989
-- | Deconstruct 'MaybeX' into an @a@ - the opposite of 'toMaybeX'. Be careful

clash-prelude/tests/Clash/Tests/Vector.hs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ case_showVectorInList =
3737
"[1 :> 2 :> Nil,3 :> 4 :> Nil]" @=? show [1 :> 2 :> Nil, 3 :> i 4 :> Nil]
3838

3939
tests :: TestTree
40-
tests = testGroup "All"
41-
[ $(testGroupGenerator)
42-
]
40+
tests = $(testGroupGenerator)
4341

4442
-- Run with:
4543
--
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{-# LANGUAGE TemplateHaskell #-}
2+
3+
module Clash.Tests.XException where
4+
5+
import Clash.XException
6+
7+
import Test.Tasty
8+
import Test.Tasty.HUnit
9+
import Test.Tasty.HUnit.Extra
10+
import Test.Tasty.TH
11+
12+
expectLeft :: HasCallStack => Either a b -> Assertion
13+
expectLeft (Left _) = pure ()
14+
expectLeft (Right _) = assertFailure "Expected Left, got Right"
15+
16+
expectRight :: HasCallStack => Either a b -> Assertion
17+
expectRight (Right _) = pure ()
18+
expectRight (Left _) = assertFailure "Expected Right, got Left"
19+
20+
case_hasX :: Assertion
21+
case_hasX = do
22+
expectRight $ hasX @(Int, Int) (1, 2)
23+
expectLeft $ hasX @(Int, Int) (x, 2)
24+
expectLeft $ hasX @(Int, Int) (1, x)
25+
expectLeft $ hasX @(Int, Int) (x, x)
26+
expectLeft $ hasX @(Int, Int) x
27+
expectExceptionNoX $ hasX @(Int, Int) (e, 2)
28+
expectExceptionNoX $ hasX @(Int, Int) (1, e)
29+
expectExceptionNoX $ hasX @(Int, Int) (e, e)
30+
expectExceptionNoX $ hasX @(Int, Int) (x, e)
31+
expectExceptionNoX $ hasX @(Int, Int) (e, x)
32+
expectExceptionNoX $ hasX @(Int, Int) e
33+
where
34+
x = errorX "X"
35+
e = error "E"
36+
37+
tests :: TestTree
38+
tests = $(testGroupGenerator)
39+
40+
-- Run with:
41+
--
42+
-- ./repld p:tests -T Clash.Tests.XException.main
43+
--
44+
-- Add -W if you want to run tests in spite of warnings
45+
--
46+
main :: IO ()
47+
main = defaultMain tests

clash-prelude/tests/Test/Tasty/HUnit/Extra.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ module Test.Tasty.HUnit.Extra
66
, expectExceptionNoX
77
) where
88

9-
import Control.DeepSeq (NFData)
109
import Control.Exception (SomeException, try, evaluate)
1110
import Test.Tasty.HUnit
1211

@@ -20,14 +19,14 @@ expectXException a0 =
2019
Right a -> assertFailure ("Expected Exception, got: " <> show a)
2120

2221
-- | Succeed if evaluating leads to an Exception
23-
expectException :: (Show a, NFData a) => a -> Assertion
22+
expectException :: Show a => a -> Assertion
2423
expectException a0 =
2524
try @SomeException (evaluate a0) >>= \case
2625
Left _ -> pure ()
2726
Right a -> assertFailure ("Expected Exception, got: " <> show a)
2827

2928
-- | Succeed if evaluating leads to a non-XException Exception
30-
expectExceptionNoX :: (Show a, NFData a) => a -> Assertion
29+
expectExceptionNoX :: Show a => a -> Assertion
3130
expectExceptionNoX a0 =
3231
try @SomeException (try @XException (evaluate a0)) >>= \case
3332
Left _ -> pure ()

clash-prelude/tests/unittests.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import qualified Clash.Tests.Signed
2424
import qualified Clash.Tests.TopEntityGeneration
2525
import qualified Clash.Tests.Unsigned
2626
import qualified Clash.Tests.Vector
27+
import qualified Clash.Tests.XException
2728

2829
import qualified Clash.Tests.Laws.Enum
2930
import qualified Clash.Tests.Laws.SaturatingNum
@@ -52,6 +53,7 @@ tests = testGroup "Unittests"
5253
, Clash.Tests.TopEntityGeneration.tests
5354
, Clash.Tests.Unsigned.tests
5455
, Clash.Tests.Vector.tests
56+
, Clash.Tests.XException.tests
5557
, testGroup "Laws"
5658
[ Clash.Tests.Laws.Enum.tests
5759
, Clash.Tests.Laws.SaturatingNum.tests

0 commit comments

Comments
 (0)