packages feed

tala-core-0.1.0: src/Progmasoft/Tala/Core/CorePrep/Verifier.hs

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

{- | Structural verifier for lowered CorePrep control flow and storage.
Verification is required after decoding artifacts and before native lowering.
-}
module Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep) where

import Data.IntSet qualified as IntSet
import Data.Set qualified as Set
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core qualified as Core
import Progmasoft.Tala.Core.CorePrep
import Progmasoft.Tala.Core.Template
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.RuntimeCall

-- | Return the unchanged module when valid, or every discovered invariant error.
verifyCorePrep :: CorePrepModule -> Either [Diagnostic] CorePrepModule
verifyCorePrep moduleValue =
    let problems =
            duplicateFunctions (corePrepModuleFunctions moduleValue)
                ++ concatMap verifyFunction (corePrepModuleFunctions moduleValue)
     in if null problems then Right moduleValue else Left problems

duplicateFunctions :: [CorePrepFunction] -> [Diagnostic]
duplicateFunctions functions = duplicateIds "TAC0001" "duplicate CorePrep function symbol" (map (resolvedSymbol . corePrepFunctionName) functions)

verifyFunction :: CorePrepFunction -> [Diagnostic]
verifyFunction function =
    let blocks = corePrepFunctionBlocks function
        blockIds = map corePrepBlockId blocks
        parameterIds = map (resolvedSymbol . fst) (corePrepFunctionParameters function)
        definitionIds = concatMap blockDefinitions blocks
     in missingEntry blockIds
            ++ verifyType "function result" (corePrepFunctionReturnType function)
            ++ concatMap (verifyParameterType . snd) (corePrepFunctionParameters function)
            ++ duplicateIds "TAC0003" "duplicate CorePrep block id" blockIds
            ++ duplicateIds "TAC0004" "duplicate CorePrep parameter symbol" parameterIds
            ++ duplicateIds "TAC0005" "CorePrep symbol is defined more than once" (parameterIds ++ definitionIds)
            ++ concatMap (verifyBlock (IntSet.fromList blockIds)) blocks
    where
        verifyParameterType = verifyType "function parameter"
        missingEntry ids =
            if corePrepFunctionEntry function `elem` ids
                then []
                else [problem "TAC0002" "CorePrep function entry block does not exist"]

blockDefinitions :: CorePrepBlock -> [SymbolId]
blockDefinitions block = [resolvedSymbol name | CorePrepBind name _ _ _ <- corePrepBlockInstructions block]

verifyBlock :: IntSet.IntSet -> CorePrepBlock -> [Diagnostic]
verifyBlock blockIds block =
    concatMap verifyInstruction (corePrepBlockInstructions block)
        ++ verifyTerminator blockIds (corePrepBlockTerminator block)

verifyInstruction :: CorePrepInstruction -> [Diagnostic]
verifyInstruction instruction = case instruction of
    CorePrepBind _ valueType _ operation -> verifyType "binding" valueType ++ verifyOperation valueType operation
    CorePrepAssign _ atom -> verifyAtom atom
    CorePrepEvaluate operation -> verifyOperation ErrorType operation

verifyOperation :: Type -> CorePrepOperation -> [Diagnostic]
verifyOperation resultType operation = case operation of
    CorePrepCopy atom -> verifyAtom atom ++ resultMismatch (atomType atom)
    CorePrepCall callee arguments -> verifyAtom callee ++ concatMap verifyAtom arguments
    CorePrepPrimitive primitive atoms -> concatMap verifyAtom atoms ++ verifyPrimitive primitive atoms resultType
    CorePrepMakeClosure function captures ->
        invalidFunction function
            ++ concatMap verifyCapture captures
            ++ closureResultMismatch resultType
    where
        resultMismatch valueType =
            if resultType == ErrorType || resultType == valueType
                then []
                else [problem "TAC0006" "CorePrep copy result type does not match its atom"]
        invalidFunction name = [problem "TAC0013" "CorePrep closure function symbol must be positive" | symbolIdValue (resolvedSymbol name) <= 0]
        closureResultMismatch valueType = case valueType of
            FunctionType _ _ -> []
            ErrorType -> []
            _ -> [problem "TAC0014" "CorePrep closure result must have a callable type"]

verifyCapture :: CorePrepCapture -> [Diagnostic]
verifyCapture (CorePrepCapture mode name valueType atom) =
    [problem "TAC0015" "CorePrep capture symbol must be positive" | symbolIdValue (resolvedSymbol name) <= 0]
        ++ [problem "TAC0016" "CorePrep capture has an unresolved type" | valueType == ErrorType]
        ++ [problem "TAC0017" "CorePrep capture atom type does not match its slot" | atomType atom /= valueType]
        ++ [ problem "TAC0018" "non-owning capture requires an AARC reference value"
           | mode /= StrongCapture && not (aarcReference valueType)
           ]
        ++ verifyType "capture" valueType
        ++ verifyAtom atom
    where
        -- Core currently lacks nominal-declaration metadata, so every resolved
        -- named type remains conservatively reference-like at this boundary.
        aarcReference value = case value of
            FunctionType _ _ -> True
            NamedType _ _ -> value /= unitType && value /= boolType && value /= intType
            _ -> False

verifyPrimitive :: Core.CorePrimitive -> [CorePrepAtom] -> Type -> [Diagnostic]
verifyPrimitive Core.CoreRuntimeCall atoms resultType =
    -- The first atom names the function; the rest are its arguments.
    case runtimeCallDefect named (map atomType (drop 1 atoms)) resultType of
        Just defect -> [problem "TAC0026" ("CorePrep " ++ runtimeDefectText defect)]
        Nothing -> []
    where
        named = case atoms of
            CorePrepLiteral (Core.CoreInteger identity) valueType : _ | valueType == intType -> runtimeFunctionOf identity
            _ -> Nothing
verifyPrimitive primitive atoms resultType
    | length atoms /= expectedArity = [problem "TAC0007" "CorePrep primitive has the wrong arity"]
    | any ((== ErrorType) . atomType) atoms = [problem "TAC0008" "CorePrep primitive contains an unresolved type"]
    | otherwise = operandProblems ++ resultProblems
    where
        expectedArity =
            if primitive `elem` [Core.CoreNegate, Core.CoreLogicalNot, Core.CoreBitwiseNot, Core.CoreMemoize] then 1 else 2
        comparisonOrLogical =
            if primitive
                `elem` [ Core.CoreLessThan
                       , Core.CoreLessEqual
                       , Core.CoreGreaterThan
                       , Core.CoreGreaterEqual
                       , Core.CoreEqual
                       , Core.CoreNotEqual
                       , Core.CoreLogicalAnd
                       , Core.CoreLogicalOr
                       , Core.CoreLogicalNot
                       , Core.CoreTypeIs
                       ]
                then True
                else False
        operandTypes = map atomType atoms
        firstType = case operandTypes of valueType : _ -> valueType; [] -> ErrorType
        operandsAgree = all (== firstType) operandTypes
        logical = primitive `elem` [Core.CoreLogicalAnd, Core.CoreLogicalOr, Core.CoreLogicalNot]
        integerOnly =
            primitive
                `elem` [ Core.CoreShiftLeft
                       , Core.CoreShiftRight
                       , Core.CoreBitwiseAnd
                       , Core.CoreBitwiseXor
                       , Core.CoreBitwiseOr
                       , Core.CoreBitwiseNot
                       ]
        numeric = all isNumericType operandTypes
        integer = all isIntegerType operandTypes
        booleanContext = all (\valueType -> valueType == boolType || isNumericType valueType) operandTypes
        negatable = isSignedIntegerType firstType || isFloatingType firstType
        operandProblems
            | primitive == Core.CoreTypeIs = case operandTypes of
                [subjectType, identityType]
                    | referenceLike subjectType && typeSpelling identityType == "uint" -> []
                    | otherwise -> [problem "TAC0024" "CorePrep type test requires a reference subject and uint identity"]
                _ -> []
            | primitive == Core.CoreMemoize = case operandTypes of
                [FunctionType [] result] | result == boolType || isNumericType result -> []
                _ ->
                    [ problem
                        "TAC0025"
                        "CorePrep memoization requires a callable without parameters whose result is bool or numeric"
                    ]
            | not operandsAgree = [problem "TAC0019" "CorePrep primitive operand types do not agree"]
            | logical && not booleanContext = [problem "TAC0020" "CorePrep logical primitive requires bool or numeric operands"]
            | integerOnly && not integer = [problem "TAC0022" "CorePrep bitwise primitive requires integer operands"]
            | primitive == Core.CoreNegate && not negatable =
                [problem "TAC0021" "CorePrep negation requires a signed integer or floating operand"]
            | not logical && not numeric && primitive `notElem` [Core.CoreEqual, Core.CoreNotEqual] =
                [problem "TAC0022" "CorePrep arithmetic or ordering primitive requires numeric operands"]
            | otherwise = []
        expectedResult
            | comparisonOrLogical = boolType
            | primitive == Core.CoreFloorDivide && isFloatingType firstType = intType
            | otherwise = firstType
        -- Evaluate deliberately discards a primitive's value, so it carries
        -- the ErrorType sentinel instead of the primitive's result type.
        resultProblems
            | resultType == ErrorType = []
            | resultType == expectedResult = []
            | otherwise = [problem "TAC0009" "CorePrep primitive result type is inconsistent"]
        referenceLike valueType = case valueType of
            FunctionType _ _ -> True
            NamedType _ _ -> not (isNumericType valueType) && valueType /= unitType
            _ -> False

verifyTerminator :: IntSet.IntSet -> CorePrepTerminator -> [Diagnostic]
verifyTerminator blockIds terminator = case terminator of
    CorePrepReturn atom -> verifyAtom atom
    CorePrepBranch atom trueTarget falseTarget -> verifyAtom atom ++ requireBool atom ++ targets [trueTarget, falseTarget]
    CorePrepJump target -> targets [target]
    CorePrepUnreachable -> []
    where
        targets values =
            [ problem "TAC0010" "CorePrep terminator targets a missing block"
            | any (`IntSet.notMember` blockIds) values
            ]
        requireBool atom = [problem "TAC0011" "CorePrep branch condition must be bool" | atomType atom /= boolType]

verifyAtom :: CorePrepAtom -> [Diagnostic]
verifyAtom atom =
    [problem "TAC0012" "CorePrep atom has an unresolved type" | atomType atom == ErrorType]
        ++ verifyType "atom" (atomType atom)
        ++ case atom of
            CorePrepLiteral literal valueType -> verifyLiteral literal valueType
            CorePrepVariable _ _ -> []

verifyLiteral :: Core.CoreLiteral -> Type -> [Diagnostic]
verifyLiteral literal valueType =
    [problem "TAC0023" "CorePrep literal payload does not match its declared scalar type" | not matches]
    where
        matches = case literal of
            Core.CoreInteger value -> integerFits valueType value
            Core.CoreFloating spelling -> isFloatingType valueType && validFloatingSpelling spelling
            Core.CoreString _ -> valueType == stringType
            Core.CoreBoolean _ -> valueType == boolType
            Core.CoreUnit -> valueType == unitType
            Core.CoreNull -> referenceLike valueType
        referenceLike value = case value of
            FunctionType _ _ -> True
            NamedType _ _ -> not (isNumericType value) && value /= unitType
            _ -> False

typeSpelling :: Type -> String
typeSpelling (NamedType (QualifiedName [Identifier name]) []) = name
typeSpelling _ = ""

integerWidths :: [(String, Bool, Int)]
integerWidths =
    [ ("char", False, 32)
    , ("byte", True, 8)
    , ("short", True, 16)
    , ("long", True, 32)
    , ("int", True, 64)
    , ("longint", True, 128)
    , ("ubyte", False, 8)
    , ("ushort", False, 16)
    , ("ulong", False, 32)
    , ("uint", False, 64)
    , ("ulongint", False, 128)
    ]

integerFits :: Type -> Integer -> Bool
integerFits valueType value = case [(signed, width) | (name, signed, width) <- integerWidths, name == typeSpelling valueType] of
    [(True, width)] -> value >= negate (2 ^ (width - 1)) && value <= 2 ^ (width - 1) - 1
    [(False, width)] -> value >= 0 && value <= 2 ^ width - 1
    _ -> False

isIntegerType :: Type -> Bool
isIntegerType valueType = any (\(name, _, _) -> name == typeSpelling valueType) integerWidths

isSignedIntegerType :: Type -> Bool
isSignedIntegerType valueType = any (\(name, signed, _) -> signed && name == typeSpelling valueType) integerWidths

isFloatingType :: Type -> Bool
isFloatingType valueType = typeSpelling valueType `elem` ["sfloat", "lfloat", "float", "double"]

isNumericType :: Type -> Bool
isNumericType valueType = isIntegerType valueType || isFloatingType valueType

validFloatingSpelling :: String -> Bool
validFloatingSpelling spelling
    | spelling `elem` ["nan", "+nan", "-nan", "inf", "+inf", "-inf"] = True
    | otherwise = case stripSign spelling of
        [] -> False
        unsignedSpelling ->
            let (mantissa, exponentPart) = break (`elem` "eE") unsignedSpelling
             in validSignificand mantissa && validExponent exponentPart
    where
        stripSign ('+' : remaining) = remaining
        stripSign ('-' : remaining) = remaining
        stripSign value = value
        validSignificand value = case break (== '.') value of
            (whole, []) -> not (null whole) && all digit whole
            (whole, _ : fraction) -> (not (null whole) || not (null fraction)) && all digit whole && all digit fraction
        validExponent [] = True
        validExponent (_ : remaining) = case stripSign remaining of
            [] -> False
            digits -> all digit digits
        digit character = character >= '0' && character <= '9'

atomType :: CorePrepAtom -> Type
atomType atom = case atom of CorePrepVariable _ valueType -> valueType; CorePrepLiteral _ valueType -> valueType

-- CorePrep may be constructed directly by tests and tooling, so it cannot
-- rely on the Core verifier having run. Revalidating structural template keys
-- here makes the stage boundary self-contained and protects Xpp lowering from
-- malformed fixed-array and value-parameter payloads.
verifyType :: String -> Type -> [Diagnostic]
verifyType context valueType = map templateProblem (validateTemplateType 128 valueType)
    where
        templateProblem issue =
            problem
                "TAC0024"
                ( "invalid CorePrep "
                    ++ context
                    ++ " template type at "
                    ++ renderPath (templateIssuePath issue)
                    ++ ": "
                    ++ templateIssueMessage issue
                )
        renderPath [] = "the type root"
        renderPath indexes = "argument " ++ concatMap (\index -> "[" ++ show index ++ "]") indexes

-- A function may have tens of thousands of blocks and symbols, so the
-- distinct values are counted through a set rather than by comparing every
-- pair.
duplicateIds :: (Ord a) => String -> String -> [a] -> [Diagnostic]
duplicateIds code message values = [problem code message | length values /= Set.size (Set.fromList values)]

problem :: String -> String -> Diagnostic
problem code message = Diagnostic CorePrepStage Error code Nothing message