ppad-fixed-0.1.3: lib/Data/Choice.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnliftedNewtypes #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE ViewPatterns #-}
-- |
-- Module: Data.Choice
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Primitives for constant-time choice.
--
-- The 'Choice' type encodes truthy and falsy values as unboxed 'Word#'
-- bit masks.
--
-- Use the standard logical primitives ('or', 'and', 'xor', 'not', eq')
-- to manipulate in-flight 'Choice' values. Use one of the selection
-- functions to use a 'Choice' to select a value in constant time,
-- or 'decide' to reduce a 'Choice' to a 'Bool' at the /end/ of a
-- sensitive computation.
module Data.Choice (
-- * Choice
Choice
, decide
, true#
, false#
, to_word#
-- * Construction
, from_full_mask#
, from_bit#
, from_word_nonzero#
, from_word_eq#
, from_word_le#
, from_word_lt#
, from_word_gt#
-- * Manipulation
, or
, and
, xor
, not
, ne
, eq
-- * Constant-time Selection
, select_word#
, select_wide#
, select_wider#
-- * Constant-time Equality
, eq_word#
, eq_wide#
, eq_wider#
) where
import qualified Data.Bits as B
import GHC.Exts (Word#, Int(..), Word(..))
import qualified GHC.Exts as Exts
import Prelude hiding (and, not, or)
-- utilities ------------------------------------------------------------------
type Limb2 = (# Word#, Word# #)
type Limb4 = (# Word#, Word#, Word#, Word# #)
-- wrapping negation
neg_w# :: Word# -> Word#
neg_w# w = Exts.plusWord# (Exts.not# w) 1##
{-# INLINE neg_w# #-}
hi# :: Word# -> Limb2
hi# w = (# 0##, w #)
{-# INLINE hi# #-}
lo# :: Word# -> Limb2
lo# w = (# w, 0## #)
{-# INLINE lo# #-}
or_w# :: Limb2 -> Limb2 -> Limb2
or_w# (# a0, a1 #) (# b0, b1 #) = (# Exts.or# a0 b0, Exts.or# a1 b1 #)
{-# INLINE or_w# #-}
and_w# :: Limb2 -> Limb2 -> Limb2
and_w# (# a0, a1 #) (# b0, b1 #) = (# Exts.and# a0 b0, Exts.and# a1 b1 #)
{-# INLINE and_w# #-}
xor_w# :: Limb2 -> Limb2 -> Limb2
xor_w# (# a0, a1 #) (# b0, b1 #) = (# Exts.xor# a0 b0, Exts.xor# a1 b1 #)
{-# INLINE xor_w# #-}
-- choice ---------------------------------------------------------------------
-- | Constant-time choice, encoded as a mask.
--
-- Note that 'Choice' is defined as an unlifted newtype, and so a
-- 'Choice' value cannot be bound at the top level. You should work
-- with it locally in the context of a computation.
--
-- Use one of the selection functions to select a 'Choice' value in
-- constant time, or 'decide' to reduce it to a 'Bool' at the /end/ of
-- a sensitive computation.
--
-- >>> decide (or# (false# ()) (true# ()))
-- True
newtype Choice = Choice Word#
-- | Construct the falsy 'Choice'.
--
-- >>> decide (false# ())
-- False
false# :: () -> Choice
false# _ = Choice 0##
{-# INLINE false# #-}
-- | Construct the truthy 'Choice'.
--
-- >>> decide (true# ())
-- True
true# :: () -> Choice
true# _ = case maxBound :: Word of
W# w -> Choice w
{-# INLINE true# #-}
-- | Decide a 'Choice' by reducing it to a 'Bool'.
--
-- The 'decide' function itself runs in constant time, but once
-- it reduces a 'Choice' to a 'Bool', any subsequent branching on
-- the result is liable to introduce variable-time behaviour.
--
-- You should 'decide' only at the /end/ of a computation, after all
-- security-sensitive computations have been carried out.
--
-- >>> decide (true# ())
-- True
decide :: Choice -> Bool
decide (Choice c) = Exts.isTrue# (Exts.neWord# c 0##)
{-# INLINE decide #-}
-- | Convert a 'Choice' to an unboxed 'Word#'.
--
-- This essentially "unboxes" the 'Choice' for direct manipulation.
--
-- >>> import qualified GHC.Exts as Exts
-- >>> Exts.isTrue# (Exts.eqWord# 0## (to_word# (false# ())))
-- True
to_word# :: Choice -> Word#
to_word# (Choice c) = Exts.and# c 1##
{-# INLINE to_word# #-}
-- construction ---------------------------------------------------------------
-- | Construct a 'Choice' from an unboxed full-word mask.
--
-- The input is /not/ checked to be a full-word mask.
--
-- >>> decide (from_full_mask# 0##)
-- False
-- >>> decide (from_full_mask# 0xFFFFFFFFF_FFFFFFFF##)
-- True
from_full_mask# :: Word# -> Choice
from_full_mask# w = Choice w
{-# INLINE from_full_mask# #-}
-- | Construct a 'Choice' from an unboxed word, which should be either
-- 0## or 1##.
--
-- The input is /not/ checked to be a bit.
--
-- >>> decide (from_bit# 1##)
-- True
from_bit# :: Word# -> Choice
from_bit# w = Choice (neg_w# w)
{-# INLINE from_bit# #-}
-- | Construct a 'Choice' from a /nonzero/ unboxed word.
--
-- The input is /not/ checked to be nonzero.
--
-- >>> decide (from_word_nonzero# 2##)
-- True
from_word_nonzero# :: Word# -> Choice
from_word_nonzero# w =
let !n = neg_w# w
!s = case B.finiteBitSize (0 :: Word) of I# m -> m Exts.-# 1#
!v = Exts.uncheckedShiftRL# (Exts.or# w n) s
in from_bit# v
{-# INLINE from_word_nonzero# #-}
-- | Construct a 'Choice' from an equality comparison.
--
-- >>> decide (from_word_eq# 0## 1##)
-- False
-- decide (from_word_eq# 1## 1##)
-- True
from_word_eq# :: Word# -> Word# -> Choice
from_word_eq# x y = case from_word_nonzero# (Exts.xor# x y) of
Choice w -> Choice (Exts.not# w)
{-# INLINE from_word_eq# #-}
-- | Construct a 'Choice from an at-most comparison.
--
-- >>> decide (from_word_le# 0## 1##)
-- True
-- >>> decide (from_word_le# 1## 1##)
-- True
from_word_le# :: Word# -> Word# -> Choice
from_word_le# x y =
let !s = case B.finiteBitSize (0 :: Word) of I# m -> m Exts.-# 1#
!bit =
Exts.uncheckedShiftRL#
(Exts.and#
(Exts.or# (Exts.not# x) y)
(Exts.or# (Exts.xor# x y) (Exts.not# (Exts.minusWord# y x))))
s
in from_bit# bit
{-# INLINE from_word_le# #-}
-- | Construct a 'Choice' from a less-than comparison.
--
-- >>> decide (from_word_lt# 0## 1##)
-- True
-- >>> decide (from_word_lt# 1## 1##)
-- False
from_word_lt# :: Word# -> Word# -> Choice
from_word_lt# x y =
let !s = case B.finiteBitSize (0 :: Word) of I# m -> m Exts.-# 1#
!bit =
Exts.uncheckedShiftRL#
(Exts.or#
(Exts.and# (Exts.not# x) y)
(Exts.and# (Exts.or# (Exts.not# x) y) (Exts.minusWord# x y)))
s
in from_bit# bit
{-# INLINE from_word_lt# #-}
-- | Construct a 'Choice' from a greater-than comparison.
--
-- >>> decide (from_word_gt# 0## 1##)
-- False
-- >>> decide (from_word_gt# 1## 1##)
-- False
from_word_gt# :: Word# -> Word# -> Choice
from_word_gt# x y = from_word_lt# y x
{-# INLINE from_word_gt# #-}
-- manipulation ---------------------------------------------------------------
-- | Logically negate a 'Choice'.
--
-- >>> decide (not (true# ()))
-- False
-- >>> decide (not (false# ()))
-- True
not :: Choice -> Choice
not (Choice w) = Choice (Exts.not# w)
{-# INLINE not #-}
-- | Logical disjunction on 'Choice' values.
--
-- >>> decide (or (true# ()) (false# ()))
-- True
or :: Choice -> Choice -> Choice
or (Choice w0) (Choice w1) = Choice (Exts.or# w0 w1)
{-# INLINE or #-}
-- | Logical conjunction on 'Choice' values.
--
-- >>> decide (and (true# ()) (false# ()))
-- False
and :: Choice -> Choice -> Choice
and (Choice w0) (Choice w1) = Choice (Exts.and# w0 w1)
{-# INLINE and #-}
-- | Logical inequality on 'Choice' values.
--
-- >>> decide (xor (true# ()) (false# ()))
-- True
xor :: Choice -> Choice -> Choice
xor (Choice w0) (Choice w1) = Choice (Exts.xor# w0 w1)
{-# INLINE xor #-}
-- | Logical inequality on 'Choice' values.
--
-- >>> decide (ne (true# ()) (false# ()))
-- True
ne :: Choice -> Choice -> Choice
ne c0 c1 = xor c0 c1
{-# INLINE ne #-}
-- | Logical equality on 'Choice' values.
--
-- >>> decide (eq (true# ()) (false# ()))
-- False
eq :: Choice -> Choice -> Choice
eq c0 c1 = not (ne c0 c1)
{-# INLINE eq #-}
-- constant-time selection ----------------------------------------------------
-- | Select an unboxed word without branching, given a 'Choice'.
--
-- >>> let w = C.select_word# 0## 1## (C.true# ()) in GHC.Word.W# w
-- 1
select_word# :: Word# -> Word# -> Choice -> Word#
select_word# a b (Choice c) = Exts.xor# a (Exts.and# c (Exts.xor# a b))
{-# INLINE select_word# #-}
-- | Select an unboxed two-limb word without branching, given a 'Choice'.
select_wide#
:: Limb2
-> Limb2
-> Choice
-> Limb2
select_wide# a b (Choice w) =
let !mask = or_w# (hi# w) (lo# w)
in xor_w# a (and_w# mask (xor_w# a b))
{-# INLINE select_wide# #-}
-- | Select an unboxed four-limb word without branching, given a 'Choice'.
select_wider#
:: Limb4
-> Limb4
-> Choice
-> Limb4
select_wider# (# a0, a1, a2, a3 #) (# b0, b1, b2, b3 #) (Choice w) =
let !w0 = Exts.xor# a0 (Exts.and# w (Exts.xor# a0 b0))
!w1 = Exts.xor# a1 (Exts.and# w (Exts.xor# a1 b1))
!w2 = Exts.xor# a2 (Exts.and# w (Exts.xor# a2 b2))
!w3 = Exts.xor# a3 (Exts.and# w (Exts.xor# a3 b3))
in (# w0, w1, w2, w3 #)
{-# INLINE select_wider# #-}
-- constant-time equality -----------------------------------------------------
-- | Compare unboxed words for equality in constant time.
--
-- >>> decide (eq_word# 0## 1##)
-- False
eq_word# :: Word# -> Word# -> Choice
eq_word# a b =
let !s = case B.finiteBitSize (0 :: Word) of I# m -> m Exts.-# 1#
!x = Exts.xor# a b
!y = Exts.uncheckedShiftRL# (Exts.or# x (neg_w# x)) s
in Choice (Exts.xor# y 1##)
{-# INLINE eq_word# #-}
-- | Compare unboxed two-limb words for equality in constant time.
--
-- >>> decide (eq_wide (# 0##, 0## #) (# 0##, 0## #))
-- True
eq_wide#
:: Limb2
-> Limb2
-> Choice
eq_wide# (# a0, a1 #) (# b0, b1 #) =
let !s = case B.finiteBitSize (0 :: Word) of I# m -> m Exts.-# 1#
!x = Exts.or# (Exts.xor# a0 b0) (Exts.xor# a1 b1)
!y = Exts.uncheckedShiftRL# (Exts.or# x (neg_w# x)) s
in Choice (Exts.xor# y 1##)
{-# INLINE eq_wide# #-}
-- | Compare unboxed four-limb words for equality in constant time.
--
-- >>> let zero = (# 0##, 0##, 0##, 0## #) in decide (eq_wider# zero zero)
-- True
eq_wider#
:: Limb4
-> Limb4
-> Choice
eq_wider# (# a0, a1, a2, a3 #) (# b0, b1, b2, b3 #) =
let !s = case B.finiteBitSize (0 :: Word) of I# m -> m Exts.-# 1#
!x = Exts.or# (Exts.or# (Exts.xor# a0 b0) (Exts.xor# a1 b1))
(Exts.or# (Exts.xor# a2 b2) (Exts.xor# a3 b3))
!y = Exts.uncheckedShiftRL# (Exts.or# x (neg_w# x)) s
in Choice (Exts.xor# y 1##)
{-# INLINE eq_wider# #-}