Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC Commits: 5c4c3bf4 by Sylvain Henry at 2026-05-02T03:39:28-04:00 testsuite: fix flaky foundation Divisible / mulIntMayOflo# tests (#27222) Since the LCG was widened to 64 bits and the seed randomised per CI run (commit 2d30f7d3400 "Vendor mini-QuickCheck for testsuite"), two latent bugs in the foundation test surface stochastically: * The Divisible property `(x `div` y) * y + (x `mod` y) == x` raises ArithException(Overflow) when (a, b) = (minBound, -1) for fixed-width signed Integral types. Split testNumber/testDivisible into Bounded and unbounded variants and skip just that one pair, gated by `(minBound :: a) < 0` so unsigned types lose no coverage. * The `mulIntMayOflo#` test compared raw Int# bit-for-bit, but the primop is only specified to return 0/non-zero -- the exact non-zero indicator legitimately differs between backends and inlining choices. Add a dedicated `testPrimopMayOflo` helper that only compares zero / non-zero. Also fix the long-standing typo "Dividible" -> "Divisible" in identifiers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> - - - - - e242ce4f by Sylvain Henry at 2026-05-02T03:39:28-04:00 testsuite: catch and display exceptions in MiniQuickCheck Exceptions raised while evaluating a property are now caught and reported as a normal failure (with arguments and seed), instead of aborting the test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> - - - - - 2 changed files: - testsuite/tests/MiniQuickCheck.hs - testsuite/tests/numeric/should_run/foundation.hs Changes: ===================================== testsuite/tests/MiniQuickCheck.hs ===================================== @@ -2,6 +2,7 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-} {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} -- | A minimal QuickCheck-like property testing framework for use in the GHC @@ -52,6 +53,8 @@ module MiniQuickCheck ) where -- base +import Control.Exception + ( SomeException, displayException, evaluate, try ) import Control.Monad.IO.Class ( liftIO ) import Data.Bits @@ -181,16 +184,39 @@ nest :: String -> ReaderT RunS IO a -> ReaderT RunS IO a nest c = local (\s -> s { depth = depth s + 1, context = c : context s }) runPropertyCheck :: PropertyCheck -> ReaderT RunS IO Result -runPropertyCheck (PropertyBinaryOp ok desc s1 s2) = - if ok - then return Success - else do - ctx <- context <$> ask - let msg = "Failure: " ++ s1 ++ " " ++ desc ++ " " ++ s2 - putMsg msg - return (Failure [msg : ctx]) -runPropertyCheck (PropertyAnd a b) = - (<>) <$> runPropertyCheck a <*> runPropertyCheck b +runPropertyCheck pcThunk = do + -- See Note [Catching exceptions in property evaluation]. + pcRes <- liftIO $ try @SomeException (evaluate pcThunk) + case pcRes of + Left e -> reportFailure ("Failure: exception: " ++ displayException e) + Right (PropertyAnd a b) -> + (<>) <$> runPropertyCheck a <*> runPropertyCheck b + Right (PropertyBinaryOp ok desc s1 s2) -> do + okRes <- liftIO $ try @SomeException (evaluate ok) + case okRes of + Right True -> return Success + Right False -> reportFailure ("Failure: " ++ s1 ++ " " ++ desc ++ " " ++ s2) + Left e -> reportFailure ("Failure: exception: " ++ displayException e) + +reportFailure :: String -> ReaderT RunS IO Result +reportFailure msg = do + ctx <- context <$> ask + putMsg msg + return (Failure [msg : ctx]) + +-- Note [Catching exceptions in property evaluation] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-- A property like `\a b -> let !r = a `div` 0 in r === b` builds a +-- `PropertyCheck` thunk whose forcing raises an exception -- in this case +-- already at the `PropertyBinaryOp` constructor, before its `ok` field is +-- ever inspected. Other properties may force `ok = (s1 == s2)` instead and +-- raise from there. +-- +-- To handle both, we `evaluate` first the `PropertyCheck` thunk and then +-- the `ok` field, each inside `try`, and report any exception through the +-- normal `reportFailure` path. The surrounding loop then still prints +-- "With arguments ... (Seed: ...)" and the test driver continues with +-- subsequent properties instead of aborting. runProperty :: Iterations -> Property -> ReaderT RunS IO Result runProperty (Iterations iters) (Prop p) = do ===================================== testsuite/tests/numeric/should_run/foundation.hs ===================================== @@ -77,13 +77,42 @@ testMultiplicative _ = Group "Multiplicative" , Property "a * b == Integer(a) * Integer(b)" $ \(a :: a) (b :: a) -> a * b === fromInteger (toInteger a * toInteger b) ] -testDividible :: forall a . (Show a, Eq a, Integral a, Num a, Arbitrary a, Typeable a) +-- | Divisibility test for Bounded Integral types (Int, Int{8,16,32,64}, +-- Word, Word{8,16,32,64}). +testDivisible :: forall a . (Show a, Eq a, Bounded a, Integral a, Num a, Arbitrary a, Typeable a) => Proxy a -> Test -testDividible _ = Group "Divisible" +testDivisible _ = Group "Divisible" + [ Property "(x `div` y) * y + (x `mod` y) == x" $ \(a :: a) (NonZero b) -> + -- See Note [Skipping minBound `div` (-1)]. + if (minBound :: a) < 0 && a == minBound && b == (-1) + then True === True + else a === (a `div` b) * b + (a `mod` b) + ] + +-- | Divisibility test for unbounded Integral types (Integer). No overflow +-- can occur here, so the property holds without exception for all NonZero b. +testDivisibleUnbounded :: forall a . (Show a, Eq a, Integral a, Num a, Arbitrary a, Typeable a) + => Proxy a -> Test +testDivisibleUnbounded _ = Group "Divisible" [ Property "(x `div` y) * y + (x `mod` y) == x" $ \(a :: a) (NonZero b) -> a === (a `div` b) * b + (a `mod` b) ] +-- Note [Skipping minBound `div` (-1)] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-- For a fixed-width *signed* Integral type, `minBound `div` (-1)` raises +-- ArithException(Overflow) because `-minBound` is not representable in the +-- type (e.g., for Int8, `-(-128)` would be 128, out of range). The div/mod +-- identity property cannot hold there, so we skip exactly that one pair. +-- +-- We detect "signed Bounded" with `(minBound :: a) < 0`: True for Int{N}, +-- False for Word{N}. This way unsigned Bounded types lose no coverage, +-- and only the genuine overflow sample is skipped for signed types. +-- +-- For the unbounded `Integer`, no overflow can occur and we use a separate +-- 'testDivisibleUnbounded' (without the Bounded constraint or the skip). +-- See #27222. + testOperatorPrecedence :: forall a . (Show a, Eq a, Prelude.Num a, Integral a, Num a, Arbitrary a, Typeable a) => Proxy a -> Test testOperatorPrecedence _ = Group "Precedence" @@ -101,14 +130,26 @@ testOperatorPrecedence _ = Group "Precedence" ] -testNumber :: (Show a, Eq a, Prelude.Num a, Integral a, Num a, Arbitrary a, Typeable a) +testNumber :: (Show a, Eq a, Prelude.Num a, Bounded a, Integral a, Num a, Arbitrary a, Typeable a) => String -> Proxy a -> Test testNumber name proxy = Group name [ testIntegral proxy , testEqOrd proxy , testAdditive proxy , testMultiplicative proxy - , testDividible proxy + , testDivisible proxy + , testOperatorPrecedence proxy + ] + +-- | Variant of 'testNumber' for unbounded Integral types (e.g., Integer). +testNumberUnbounded :: (Show a, Eq a, Prelude.Num a, Integral a, Num a, Arbitrary a, Typeable a) + => String -> Proxy a -> Test +testNumberUnbounded name proxy = Group name + [ testIntegral proxy + , testEqOrd proxy + , testAdditive proxy + , testMultiplicative proxy + , testDivisibleUnbounded proxy , testOperatorPrecedence proxy ] @@ -119,7 +160,7 @@ testNumberRefs = Group "ALL" , testNumber "Int16" (Proxy :: Proxy Int16) , testNumber "Int32" (Proxy :: Proxy Int32) , testNumber "Int64" (Proxy :: Proxy Int64) - , testNumber "Integer" (Proxy :: Proxy Integer) + , testNumberUnbounded "Integer" (Proxy :: Proxy Integer) , testNumber "Word" (Proxy :: Proxy Word) , testNumber "Word8" (Proxy :: Proxy Word8) , testNumber "Word16" (Proxy :: Proxy Word16) @@ -399,7 +440,7 @@ testPrimops = Group "primop" , testPrimop "-#" (Primop.-#) (Wrapper.-#) , testPrimop "*#" (Primop.*#) (Wrapper.*#) , testPrimop "timesInt2#" Primop.timesInt2# Wrapper.timesInt2# - , testPrimop "mulIntMayOflo#" Primop.mulIntMayOflo# Wrapper.mulIntMayOflo# + , testPrimopMayOflo "mulIntMayOflo#" Primop.mulIntMayOflo# Wrapper.mulIntMayOflo# , testPrimopDivLike "quotInt#" Primop.quotInt# Wrapper.quotInt# , testPrimopDivLike "remInt#" Primop.remInt# Wrapper.remInt# , testPrimopDivLike "quotRemInt#" Primop.quotRemInt# Wrapper.quotRemInt# @@ -497,6 +538,31 @@ instance TestPrimop (Int# -> Int# -> Int#) where testPrimopDivLike s l r = Property s $ twoNonZero $ \ (uInt#-> x0) (uInt#-> x1) -> wInt# (l x0 x1) === wInt# (r x0 x1) testPrimopShift s l r = Property s $ \ (uInt#-> x0) (BoundedShiftAmount @Int shift) -> wInt# (l x0 (uInt# shift)) === wInt# (r x0 (uInt# shift)) +-- | Compare two 'mulIntMayOflo#'-like primops only on whether their result +-- is zero. See Note [Comparing mulIntMayOflo# results]. +testPrimopMayOflo :: String + -> (Int# -> Int# -> Int#) + -> (Int# -> Int# -> Int#) + -> Test +testPrimopMayOflo s l r = + Property s $ \ (uInt# -> x0) (uInt# -> x1) -> + (wInt# (l x0 x1) == 0) === (wInt# (r x0 x1) == 0) + +-- Note [Comparing mulIntMayOflo# results] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-- The 'mulIntMayOflo#' primop is only specified to return 0 if the signed +-- multiplication does not overflow, and a non-zero value if it /may/ +-- overflow (see Note [MO_S_MulMayOflo significant width] in +-- GHC.Cmm.MachOp). The exact non-zero value is unspecified and legitimately +-- differs between backends and between inlined vs. non-inlined call sites +-- (e.g., the LLVM backend's `isSMulOK` returns `sext_signbit(low) - high`, +-- which is some arbitrary non-zero word on overflow). +-- +-- Comparing the raw Int# results bit-for-bit is therefore too strict and +-- causes spurious test failures whenever the random arguments happen to +-- overflow. We compare zero/non-zero instead, which matches the spec. +-- See #27222. + instance TestPrimop (Int# -> Int# -> (# Int#,Int# #)) where testPrimop s l r = Property s $ \ (uInt#-> x0) (uInt#-> x1) -> WTUP2(wInt#,wInt#, (l x0 x1)) === WTUP2(wInt#,wInt#, (r x0 x1)) testPrimopDivLike s l r = Property s $ twoNonZero $ \ (uInt#-> x0) (uInt#-> x1) -> WTUP2(wInt#,wInt#, (l x0 x1)) === WTUP2(wInt#,wInt#, (r x0 x1)) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/391413435eb02d9d8ed8bb85946de2d... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/391413435eb02d9d8ed8bb85946de2d... You're receiving this email because of your account on gitlab.haskell.org.