eproc

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 67999374211a038876017be1b90c74742ebc555e
parent b0a7a7cf7abdbae101d2efb87bcaf13a132b7501
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 28 Jun 2026 21:17:23 -0230

reject non-finite parameters in config constructors

NaN and +/-Infinity were slipping past the existing range checks
(NaN comparisons are False, and -Inf < +Inf passes 'lo < hi'),
leaving the door open to e.g. infinite bounds that silently
zeroed the safe-bet ceilings. Tighten each smart constructor to
require finite m/lo/hi/alpha (Bounded), lo/hi/alpha (Paired),
and p0/alpha (Bernoulli) via a shared 'finite' helper in Common.

Diffstat:
Mlib/Numeric/Eproc/Bernoulli.hs | 17+++++++++++------
Mlib/Numeric/Eproc/Bounded.hs | 19++++++++++++-------
Mlib/Numeric/Eproc/Common.hs | 10++++++++++
Mlib/Numeric/Eproc/Paired.hs | 4++--
Mtest/Main.hs | 16++++++++++++++++
5 files changed, 51 insertions(+), 15 deletions(-)

diff --git a/lib/Numeric/Eproc/Bernoulli.hs b/lib/Numeric/Eproc/Bernoulli.hs @@ -76,6 +76,7 @@ module Numeric.Eproc.Bernoulli ( import Numeric.Eproc.Common ( Bettor(..), Verdict(..), ConfigError(..) , BetState, init_bet, bet_lambda, step_bet + , finite ) -- types ---------------------------------------------------------------------- @@ -128,9 +129,11 @@ data State = State { -- to leave numerical margin -- the WSR safety recommendation. -- -- Returns 'Left' with a 'ConfigError' on inputs that would leave --- the mathematical regime: @p_0@ outside @(0, 1)@ (the degenerate --- case @p_0 = 0@ would make @lambda_max@ infinite, and @p_0 = 1@ --- leaves no room for an alternative), or @alpha@ outside @(0, 1)@. +-- the mathematical regime: either of @p_0@ or @alpha@ non-finite +-- (NaN or infinite); @p_0@ outside @(0, 1)@ (the degenerate case +-- @p_0 = 0@ would make @lambda_max@ infinite, and @p_0 = 1@ +-- leaves no room for an alternative); or @alpha@ outside +-- @(0, 1)@. -- -- >>> let Right cfg = config 0.05 1.0e-3 Newton config @@ -139,9 +142,11 @@ config -> Bettor -- ^ bettor strategy -> Either ConfigError Config config !p0 !alpha !b - | not (p0 > 0 && p0 < 1) = Left (InvalidBaselineRate p0) - | not (alpha > 0 && alpha < 1) = Left (InvalidAlpha alpha) - | otherwise = Right Config { + | not (finite p0 && p0 > 0 && p0 < 1) = + Left (InvalidBaselineRate p0) + | not (finite alpha && alpha > 0 && alpha < 1) = + Left (InvalidAlpha alpha) + | otherwise = Right Config { cfg_bettor = b , cfg_lam_max = 0.5 / p0 , cfg_p0 = p0 diff --git a/lib/Numeric/Eproc/Bounded.hs b/lib/Numeric/Eproc/Bounded.hs @@ -75,6 +75,7 @@ module Numeric.Eproc.Bounded ( import Numeric.Eproc.Common ( Bettor(..), Verdict(..), ConfigError(..) , BetState, init_bet, bet_lambda, step_bet + , finite ) -- types ---------------------------------------------------------------------- @@ -149,9 +150,10 @@ data State = State { -- adjustment for the two one-sided e-processes. -- -- Returns 'Left' with a 'ConfigError' on inputs that would leave --- the mathematical regime: @alpha@ outside @(0, 1)@, @lo >= hi@, --- or @m@ outside the open interval @(lo, hi)@ (strict, to avoid --- the safe-bet ceilings dividing by zero). +-- the mathematical regime: any of @m@, @lo@, @hi@, @alpha@ +-- non-finite (NaN or infinite); @alpha@ outside @(0, 1)@; +-- @lo >= hi@; or @m@ outside the open interval @(lo, hi)@ +-- (strict, to avoid the safe-bet ceilings dividing by zero). -- -- >>> let Right cfg = config 0.5 0.0 1.0 1.0e-3 Newton config @@ -162,10 +164,13 @@ config -> Bettor -- ^ bettor strategy -> Either ConfigError Config config !m !lo !hi !alpha !b - | not (alpha > 0 && alpha < 1) = Left (InvalidAlpha alpha) - | not (lo < hi) = Left (InvalidBounds lo hi) - | not (lo < m && m < hi) = Left (InvalidNullMean m lo hi) - | otherwise = Right Config { + | not (finite alpha && alpha > 0 && alpha < 1) = + Left (InvalidAlpha alpha) + | not (finite lo && finite hi && lo < hi) = + Left (InvalidBounds lo hi) + | not (finite m && lo < m && m < hi) = + Left (InvalidNullMean m lo hi) + | otherwise = Right Config { cfg_bettor = b , cfg_lam_max_pos = 0.5 / (m - lo) , cfg_lam_max_neg = 0.5 / (hi - m) diff --git a/lib/Numeric/Eproc/Common.hs b/lib/Numeric/Eproc/Common.hs @@ -29,6 +29,9 @@ module Numeric.Eproc.Common ( , init_bet , bet_lambda , step_bet + + -- * Internal: helpers + , finite ) where -- | A predictable bettor. @@ -107,6 +110,13 @@ data ConfigError = | InvalidBaselineRate {-# UNPACK #-} !Double deriving (Eq, Show) +-- | True iff the argument is a finite IEEE-754 double (not NaN, not +-- @+\/-Infinity@). Used by the @config@ smart constructors to keep +-- the bounded-random-variable promise honest. +finite :: Double -> Bool +finite x = not (isNaN x) && not (isInfinite x) +{-# INLINE finite #-} + -- | Per-bettor state. One constructor per 'Bettor' alternative; the -- constructor used in any given state matches the 'Bettor' chosen -- in the enclosing 'Config'. diff --git a/lib/Numeric/Eproc/Paired.hs b/lib/Numeric/Eproc/Paired.hs @@ -90,8 +90,8 @@ newtype State = State Bounded.State -- mean @0@. -- -- Returns 'Left' with a 'ConfigError' on inputs that would leave --- the mathematical regime: @lo >= hi@ or @alpha@ outside --- @(0, 1)@. +-- the mathematical regime: any of @lo@, @hi@, @alpha@ non-finite +-- (NaN or infinite); @lo >= hi@; or @alpha@ outside @(0, 1)@. -- -- >>> let Right cfg = config 0.0 1.0 1.0e-3 Newton config diff --git a/test/Main.hs b/test/Main.hs @@ -368,8 +368,24 @@ config_validation_tests = testGroup "config validation" [ assertLeft (P.config 0.0 1.0 0.0 Bounded.Newton) , testCase "Paired: lo >= hi rejected" $ assertLeft (P.config 1.0 0.0 0.01 Bounded.Newton) + , testCase "Bounded: infinite bounds rejected" $ + assertLeft (Bounded.config 0.0 nInf pInf 0.01 Bounded.Newton) + , testCase "Bounded: NaN m rejected" $ + assertLeft (Bounded.config nan 0.0 1.0 0.01 Bounded.Newton) + , testCase "Bounded: NaN alpha rejected" $ + assertLeft (Bounded.config 0.5 0.0 1.0 nan Bounded.Newton) + , testCase "Bernoulli: NaN p0 rejected" $ + assertLeft (Bern.config nan 0.01 Bern.Newton) + , testCase "Bernoulli: infinite alpha rejected" $ + assertLeft (Bern.config 0.05 pInf Bern.Newton) + , testCase "Paired: infinite hi rejected" $ + assertLeft (P.config 0.0 pInf 0.01 Bounded.Newton) ] where + nan, pInf, nInf :: Double + nan = 0 / 0 + pInf = 1 / 0 + nInf = negate (1 / 0) assertLeft :: Either C.ConfigError a -> Assertion assertLeft e = case e of Left _ -> pure ()