packages feed

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

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

{- | Central numeric operator and contextual-literal policy.

Lexer and parser establish literal identity. This module answers semantic
questions without constructing diagnostics, which keeps it usable by the
TypeChecker, Analyzer, and future constant evaluator.
-}
module Progmasoft.Tala.NumericSemantics
    ( NumericContext (..)
    , NumericRuleError (..)
    , NumericRuleResult (..)
    , integerLiteralRule
    , floatingLiteralRule
    , unaryNumericRule
    , binaryNumericRule
    , acceptsBooleanContext
    , renderNumericRuleError
    ) where

import Progmasoft.Tala.AST
import Progmasoft.Tala.BuiltinTypes

-- | Type context available while applying a literal or operator rule.
data NumericContext
    = -- | No destination type constrains the expression.
      NoNumericContext
    | -- | A concrete expected scalar type is available.
      TargetNumericType Type
    | -- | The expression is consumed as a condition.
      BooleanNumericContext
    deriving (Eq, Ord, Read, Show)

-- | Specific reason that a contextual literal or numeric operator is invalid.
data NumericRuleError
    = -- | Integer does not fit its selected scalar type.
      IntegerLiteralOutsideTarget ScalarType Integer
    | -- | Uncontextualized literal exceeds the default @int@ range.
      UntargetedIntegerOutsideInt Integer
    | -- | Expected target is not a floating-point type.
      FloatingLiteralRequiresFloatingTarget Type
    | -- | Unary operator received a non-numeric operand.
      UnaryRequiresNumeric UnaryOperator Type
    | -- | Bitwise operator received a non-integer operand.
      UnaryRequiresInteger UnaryOperator Type
    | -- | Negation received an unsigned or non-numeric operand.
      NegationRequiresSignedNumeric Type
    | -- | Logical negation operand is not condition-compatible.
      LogicalRequiresBooleanContext UnaryOperator Type
    | -- | Binary operands have different types.
      BinaryRequiresMatchingTypes BinaryOperator Type Type
    | -- | Arithmetic operator received a non-numeric type.
      BinaryRequiresNumericType BinaryOperator Type
    | -- | Integer-only operator received a non-integer type.
      BinaryRequiresIntegerType BinaryOperator Type
    | -- | Logical operands are not condition-compatible.
      BinaryRequiresBooleanContext BinaryOperator Type Type
    deriving (Eq, Ord, Read, Show)

-- | Result type and optional validation error produced by a numeric rule.
data NumericRuleResult = NumericRuleResult
    { numericRuleType :: Type
    -- ^ Type to assign even when the rule reports an error.
    , numericRuleError :: Maybe NumericRuleError
    -- ^ Failure details, or @Nothing@ on success.
    }
    deriving (Eq, Ord, Read, Show)

{- | Select and range-check the type of an integer literal from its context.
Boolean conditions retain the language's documented numeric condition rule;
an unconstrained literal defaults to @int@ and must fit that exact range.
-}
integerLiteralRule :: NumericContext -> Integer -> NumericRuleResult
integerLiteralRule context value = case context of
    BooleanNumericContext -> success boolType
    TargetNumericType target -> case typeToScalarType target of
        Just BooleanScalar -> success boolType
        Just scalar
            | scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily] ->
                if integerFits scalar value
                    then success (scalarTypeToType scalar)
                    else failure (scalarTypeToType scalar) (IntegerLiteralOutsideTarget scalar value)
        _ -> defaultInteger
    NoNumericContext -> defaultInteger
    where
        defaultInteger
            | integerFits defaultIntegerScalar value = success (scalarTypeToType defaultIntegerScalar)
            | otherwise = failure (scalarTypeToType defaultIntegerScalar) (UntargetedIntegerOutsideInt value)

-- | Select a floating literal type, requiring any explicit target to be float.
floatingLiteralRule :: NumericContext -> NumericRuleResult
floatingLiteralRule context = case context of
    TargetNumericType target -> case typeToScalarType target of
        Just scalar | scalarTypeFamily scalar == FloatingFamily -> success (scalarTypeToType scalar)
        _ -> failure (scalarTypeToType defaultFloatingScalar) (FloatingLiteralRequiresFloatingTarget target)
    _ -> success (scalarTypeToType defaultFloatingScalar)

-- | Apply the type rule for a unary numeric or logical operator.
unaryNumericRule :: UnaryOperator -> Type -> NumericRuleResult
unaryNumericRule operator operandType = case operator of
    LogicalNot
        | acceptsBooleanContext operandType -> success boolType
        | otherwise -> failure boolType (LogicalRequiresBooleanContext operator operandType)
    BitwiseNot
        | isIntegerType operandType -> success operandType
        | otherwise -> failure operandType (UnaryRequiresInteger operator operandType)
    UnaryPlus
        | isNumericType operandType -> success operandType
        | otherwise -> failure operandType (UnaryRequiresNumeric operator operandType)
    UnaryNegate
        | isSignedIntegerType operandType || isFloatingType operandType -> success operandType
        | otherwise -> failure operandType (NegationRequiresSignedNumeric operandType)

-- | Apply operand compatibility and result-type rules for a binary operator.
binaryNumericRule :: BinaryOperator -> Type -> Type -> NumericRuleResult
binaryNumericRule operator leftType rightType
    | operator `elem` [LogicalAnd, LogicalOr] =
        if acceptsBooleanContext leftType && acceptsBooleanContext rightType
            then success boolType
            else failure boolType (BinaryRequiresBooleanContext operator leftType rightType)
    | operator `elem` [Equal, NotEqual] =
        if leftType == rightType
            then success boolType
            else failure boolType (BinaryRequiresMatchingTypes operator leftType rightType)
    | leftType /= rightType =
        failure (resultFor operator leftType) (BinaryRequiresMatchingTypes operator leftType rightType)
    | operator `elem` [ShiftLeft, ShiftRight, BitwiseAnd, BitwiseXor, BitwiseOr] && not (isIntegerType leftType) =
        failure leftType (BinaryRequiresIntegerType operator leftType)
    | not (isNumericType leftType) =
        failure (resultFor operator leftType) (BinaryRequiresNumericType operator leftType)
    | otherwise = success (resultFor operator leftType)

-- | Test whether a type is accepted as a condition by the current language rules.
acceptsBooleanContext :: Type -> Bool
acceptsBooleanContext valueType = valueType == boolType || isNumericType valueType

resultFor :: BinaryOperator -> Type -> Type
resultFor operator operandType
    | operator `elem` [LessThan, LessEqual, GreaterThan, GreaterEqual, Equal, NotEqual] = boolType
    | operator == FloorDivide && isFloatingType operandType = intType
    | otherwise = operandType

success :: Type -> NumericRuleResult
success valueType = NumericRuleResult valueType Nothing

failure :: Type -> NumericRuleError -> NumericRuleResult
failure valueType issue = NumericRuleResult valueType (Just issue)

-- | Render a numeric rule failure as concise user-facing diagnostic text.
renderNumericRuleError :: NumericRuleError -> String
renderNumericRuleError issue = case issue of
    IntegerLiteralOutsideTarget scalar value ->
        "integer literal " ++ show value ++ " does not fit target type " ++ scalarTypeName scalar
    UntargetedIntegerOutsideInt value ->
        "un-targeted integer literal " ++ show value ++ " does not fit int; provide an explicit wider target"
    FloatingLiteralRequiresFloatingTarget target ->
        "floating-point literal cannot use non-floating target " ++ show target
    UnaryRequiresNumeric operator operand ->
        show operator ++ " requires a numeric operand, found " ++ show operand
    UnaryRequiresInteger operator operand ->
        show operator ++ " requires an integer operand, found " ++ show operand
    NegationRequiresSignedNumeric operand ->
        "unary negation requires a signed integer or floating-point operand, found " ++ show operand
    LogicalRequiresBooleanContext operator operand ->
        show operator ++ " requires bool or numeric context, found " ++ show operand
    BinaryRequiresMatchingTypes operator left right ->
        show operator ++ " requires matching operand types, found " ++ show left ++ " and " ++ show right
    BinaryRequiresNumericType operator operand ->
        show operator ++ " requires numeric operands, found " ++ show operand
    BinaryRequiresIntegerType operator operand ->
        show operator ++ " requires integer operands, found " ++ show operand
    BinaryRequiresBooleanContext operator left right ->
        show operator ++ " requires bool or numeric operands, found " ++ show left ++ " and " ++ show right