packages feed

tala-frontend-0.1.0: src/Progmasoft/Tala/IntegerEvaluation.hs

-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1

{- | Bounded, exact integer operations used while evaluating compile-time
expressions. These limits protect compiler availability; they do not narrow the
runtime integer types or the accepted integer-literal grammar.

Every public operation in this module either returns an exact value strictly
inside the magnitude bound or reports the same resource-limit error. Callers
translate that error into a diagnostic appropriate to their source context.
The helpers deliberately accept `Integer` operands so no conversion to a host
word can happen before validation.
-}
module Progmasoft.Tala.IntegerEvaluation
    ( CompileTimeIntegerError (..)
    , checkCompileTimeInteger
    , multiplyCompileTimeIntegers
    , evaluateCompileTimePower
    , evaluateCompileTimeShiftLeft
    , evaluateCompileTimeShiftRight
    ) where

import Data.Bits (countLeadingZeros, shiftL, shiftR)
import Data.Word (Word64)

-- | Failures returned by bounded compile-time integer operations.
data CompileTimeIntegerError
    = -- | The exact result exceeds the 65,536-bit bound.
      CompileTimeIntegerLimitExceeded
    | -- | Integer exponentiation was given a negative exponent.
      CompileTimeNegativeExponent
    deriving (Eq, Ord, Read, Show)

-- This is intentionally much wider than any built-in scalar (128 bits). A
-- fixed ceiling also bounds intermediate products before they can exhaust the
-- host while processing untrusted source or a project artifact. The lower edge
-- is exclusive, matching the positive bound and avoiding an asymmetric domain.
maximumCompileTimeIntegerBits :: Int
maximumCompileTimeIntegerBits = 65536

maximumCompileTimeMagnitude :: Integer
maximumCompileTimeMagnitude = 1 `shiftL` maximumCompileTimeIntegerBits

-- | Accept an integer strictly inside the module's symmetric magnitude bound.
checkCompileTimeInteger :: Integer -> Either CompileTimeIntegerError Integer
checkCompileTimeInteger value
    | value <= negate maximumCompileTimeMagnitude || value >= maximumCompileTimeMagnitude =
        Left CompileTimeIntegerLimitExceeded
    | otherwise = Right value

{- | Reject products that cannot fit before constructing a potentially twice-
as-wide intermediate. The one-bit lower bound is exact for powers of two;
borderline products are still checked after multiplication. Taking absolute
values makes the same proof valid for all four sign combinations. If the lower
bound does not prove overflow, the exact product is constructed once and then
checked against the half-open limit; this keeps the fast rejection conservative.
-}

-- | Multiply exact integers after checking operand and result resource bounds.
multiplyCompileTimeIntegers :: Integer -> Integer -> Either CompileTimeIntegerError Integer
multiplyCompileTimeIntegers left right = do
    boundedLeft <- checkCompileTimeInteger left
    boundedRight <- checkCompileTimeInteger right
    if boundedLeft == 0 || boundedRight == 0
        then Right 0
        else
            if integerBitLength (abs boundedLeft) + integerBitLength (abs boundedRight) - 1 > maximumCompileTimeIntegerBits
                then Left CompileTimeIntegerLimitExceeded
                else checkCompileTimeInteger (boundedLeft * boundedRight)

-- | Compute an exact non-negative integer power by bounded repeated squaring.
evaluateCompileTimePower :: Integer -> Integer -> Either CompileTimeIntegerError Integer
evaluateCompileTimePower base exponentValue
    | exponentValue < 0 = Left CompileTimeNegativeExponent
    | otherwise = do
        boundedBase <- checkCompileTimeInteger base
        case boundedBase of
            0 -> Right (if exponentValue == 0 then 1 else 0)
            1 -> Right 1
            -1 -> Right (if even exponentValue then 1 else -1)
            _ -> powerLoop 1 boundedBase exponentValue
    where
        -- Repeated squaring makes the number of iterations logarithmic in the
        -- exponent. Multiplication itself performs the resource preflight, so
        -- an enormous exponent cannot force construction of its final power.
        powerLoop accumulated factor remaining
            | remaining == 0 = checkCompileTimeInteger accumulated
            | otherwise = do
                nextAccumulated <-
                    if odd remaining
                        then multiplyCompileTimeIntegers accumulated factor
                        else Right accumulated
                let nextRemaining = remaining `quot` 2
                if nextRemaining == 0
                    then checkCompileTimeInteger nextAccumulated
                    else do
                        squaredFactor <- multiplyCompileTimeIntegers factor factor
                        powerLoop nextAccumulated squaredFactor nextRemaining

-- | Shift an exact integer left, interpreting a negative count as right shift.
evaluateCompileTimeShiftLeft :: Integer -> Integer -> Either CompileTimeIntegerError Integer
evaluateCompileTimeShiftLeft value amount = do
    boundedValue <- checkCompileTimeInteger value
    if amount < 0
        then evaluateCompileTimeShiftRight boundedValue (negate amount)
        else
            if boundedValue == 0
                then Right 0
                else do
                    -- Check the resulting bit width before asking the bignum
                    -- backend to allocate the shifted representation.
                    if toInteger (integerBitLength (abs boundedValue)) + amount > toInteger maximumCompileTimeIntegerBits
                        then Left CompileTimeIntegerLimitExceeded
                        else pure ()
                    boundedAmount <- checkedShiftAmount amount
                    checkCompileTimeInteger (shiftL boundedValue boundedAmount)

-- | Arithmetic-shift an exact integer right, interpreting a negative count as left shift.
evaluateCompileTimeShiftRight :: Integer -> Integer -> Either CompileTimeIntegerError Integer
evaluateCompileTimeShiftRight value amount = do
    boundedValue <- checkCompileTimeInteger value
    if amount < 0
        then evaluateCompileTimeShiftLeft boundedValue (negate amount)
        else
            if amount >= toInteger maximumCompileTimeIntegerBits
                -- Past the largest possible magnitude, arithmetic right shift
                -- contains only the sign fill and needs no large temporary.
                then Right (if boundedValue < 0 then -1 else 0)
                else do
                    boundedAmount <- checkedShiftAmount amount
                    checkCompileTimeInteger (shiftR boundedValue boundedAmount)

checkedShiftAmount :: Integer -> Either CompileTimeIntegerError Int
checkedShiftAmount amount
    | amount < 0 || amount > toInteger maximumCompileTimeIntegerBits = Left CompileTimeIntegerLimitExceeded
    | otherwise = Right (fromInteger amount)

-- Callers validate magnitudes before asking for their width, so this recursion
-- visits at most 1,024 fixed-size limbs at the 65,536-bit ceiling. Using
-- Word64 as the limb definition keeps the result independent of host Int width
-- and avoids narrowing the complete arbitrary-precision value in one cast.
integerBitLength :: Integer -> Int
integerBitLength value
    | value <= 0 = 0
    | value <= toInteger (maxBound :: Word64) = 64 - countLeadingZeros (fromInteger value :: Word64)
    | otherwise = 64 + integerBitLength (value `shiftR` 64)