packages feed

tala-compiler-0.1.0: test/CoreInterpreter.hs

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

{- | A small reference evaluator for scalar Core, used only by tests.

It executes Core statements directly, without CorePrep, the optimizer or the
native backend, so a test can state what a source program must compute and
check that against the Core the Desugarer produced and, separately, against
the Core the optimizer left. It is deliberately independent of both: it
shares no lowering or rewriting code with the compiler.

The subset is the one the tests need: integer and Boolean values, the
arithmetic, comparison, bitwise and logical primitives, expression-local
bindings, conditional expressions, direct calls of module functions,
closures, callables that remember their result, strings, the runtime calls
for text and the console, and all statement forms. What a program writes to
the console is kept and returned with its result. Integers are unbounded; a test that depends on overflow
belongs to the native execution tests instead. Anything outside the subset,
and any run that exceeds its step budget, yields 'Nothing' rather than a
guess.
-}
module CoreInterpreter
    ( Value (..)
    , Written (..)
    , runFunction
    , runFunctionWithBudget
    , runFunctionWriting
    ) where

import Data.Bits (complement, shiftL, shiftR, xor, (.&.), (.|.))
import Data.Char (chr, isDigit)
import Data.Ratio ((%))
import RuntimeText
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.RuntimeCall

-- | A runtime value of the supported subset.
data Value
    = IntegerValue Integer
    | BooleanValue Bool
    | UnitValue
    | {- | A closure: the values its captures had when it was created, the
      symbols of its parameters, and its body. A capture initializer is
      evaluated once, where the closure is created.
      -}
      ClosureValue [(Int, Value)] [Int] [CoreStatement]
    | {- | A callable that remembers its result: the cell that holds the
      result once there is one, and the callable that computes it. Copies of
      the value name the same cell, which is how they share the result.
      -}
      MemoValue Int Value
    | -- | A string.
      TextValue String
    | {- | A floating-point number: whether it is negative zero, and its exact
      value, which is that of the binary number its type holds.
      -}
      FloatingValue Bool Rational
    deriving (Eq, Show)

-- | What a run wrote to the console, in the order it was written.
data Written = Written
    { writtenOutput :: String
    -- ^ Standard output.
    , writtenError :: String
    -- ^ Standard error.
    }
    deriving (Eq, Show)

-- Local values keyed by symbol identity. Core symbols are unique within a
-- function, so one flat table is a faithful model of its locals.
type Locals = [(Int, Value)]

{- | What a run carries from step to step besides its locals.

The steps left bound every run, so a lowering error that turns a loop into
an endless one fails the test instead of hanging the suite. The cells hold
the results that callables have remembered; they belong to the run and not
to a function, because a callable is passed between functions and must find
its result wherever it is called.
-}
data Budget = Budget
    { stepsLeft :: Int
    , cells :: [(Int, Value)]
    , nextCell :: Int
    , writes :: [(Bool, String)]
    -- ^ Every console write, latest first: whether it went to standard
    -- error, and the text.
    }

-- | Whether no step is left.
exhausted :: Budget -> Bool
exhausted budget = stepsLeft budget <= 0

-- | The budget after one step.
step :: Budget -> Budget
step budget = budget {stepsLeft = stepsLeft budget - 1}

data Flow
    = Proceed
    | BreakLoop
    | ContinueLoop
    | ReturnValue Value

-- | Run a function by name with the default step budget.
runFunction :: CoreModule -> String -> [Value] -> Maybe Value
runFunction = runFunctionWithBudget 200000

{- | Run a function by name; 'Nothing' when it is missing, leaves the
supported subset, or exhausts the budget.
-}
runFunctionWithBudget :: Int -> CoreModule -> String -> [Value] -> Maybe Value
runFunctionWithBudget steps moduleValue name arguments = fst <$> runFunctionWriting steps moduleValue name arguments

{- | Run a function by name and return what it wrote to the console with its
result. Nothing is returned for a run that does not finish: what such a run
wrote before it stopped is not a result a test may rely on here.
-}
runFunctionWriting :: Int -> CoreModule -> String -> [Value] -> Maybe (Value, Written)
runFunctionWriting steps moduleValue name arguments = do
    function <- firstJust [function | function <- coreModuleFunctions moduleValue, functionSpelling function == name]
    (value, budget) <- call moduleValue (Budget steps [] 0 []) function arguments
    let inOrder = reverse (writes budget)
    Just
        ( value
        , Written
            (concat [written | (False, written) <- inOrder])
            (concat [written | (True, written) <- inOrder])
        )

functionSpelling :: CoreFunction -> String
functionSpelling = identifierText . resolvedSpelling . coreFunctionName

firstJust :: [value] -> Maybe value
firstJust values = case values of
    value : _ -> Just value
    [] -> Nothing

symbolOf :: ResolvedName -> Int
symbolOf = symbolIdValue . resolvedSymbol

call :: CoreModule -> Budget -> CoreFunction -> [Value] -> Maybe (Value, Budget)
call moduleValue budget function arguments
    | exhausted budget = Nothing
    | length arguments /= length (coreFunctionParameters function) = Nothing
    | otherwise = do
        let locals = zip (map (symbolOf . fst) (coreFunctionParameters function)) arguments
        (flow, _, remaining) <- executeAll moduleValue (step budget) locals (coreFunctionBody function)
        case flow of
            ReturnValue value -> Just (value, remaining)
            Proceed -> Just (UnitValue, remaining)
            _ -> Nothing

executeAll :: CoreModule -> Budget -> Locals -> [CoreStatement] -> Maybe (Flow, Locals, Budget)
executeAll _ budget locals [] = Just (Proceed, locals, budget)
executeAll moduleValue budget locals (statement : remaining) = do
    (flow, nextLocals, nextBudget) <- execute moduleValue budget locals statement
    case flow of
        Proceed -> executeAll moduleValue nextBudget nextLocals remaining
        _ -> Just (flow, nextLocals, nextBudget)

execute :: CoreModule -> Budget -> Locals -> CoreStatement -> Maybe (Flow, Locals, Budget)
execute moduleValue budget locals statement
    | exhausted budget = Nothing
    | otherwise = case statement of
        CoreBind binding -> do
            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals (coreBindingValue binding)
            Just (Proceed, store (symbolOf (coreBindingName binding)) value afterLocals, afterBudget)
        CoreAssign name expression -> do
            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals expression
            -- An assignment never introduces a local: a missing target is a
            -- lowering error, not something to paper over.
            _ <- lookup (symbolOf name) afterLocals
            Just (Proceed, store (symbolOf name) value afterLocals, afterBudget)
        CoreReturn expression -> do
            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals expression
            Just (ReturnValue value, afterLocals, afterBudget)
        CoreEvaluate expression -> do
            (_, afterLocals, afterBudget) <- evaluate moduleValue spent locals expression
            Just (Proceed, afterLocals, afterBudget)
        CoreIf condition whenTrue whenFalse -> do
            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals condition
            selected <- truth value
            executeAll moduleValue afterBudget afterLocals (if selected then whenTrue else whenFalse)
        CoreWhile condition body -> loop moduleValue spent locals (Just condition) body [] True
        CoreDoWhile body condition -> loop moduleValue spent locals (Just condition) body [] False
        CoreFor condition body update -> loop moduleValue spent locals (Just condition) body update True
        CoreBreak -> Just (BreakLoop, locals, spent)
        CoreContinue -> Just (ContinueLoop, locals, spent)
    where
        spent = step budget

{- | Run one structured loop.

The condition is tested before the body when @testFirst@ holds and after it
otherwise. @continue@ ends the body, runs the update clause and then tests
the condition, in every loop form.
-}
loop ::
    CoreModule ->
    Budget ->
    Locals ->
    Maybe CoreExpression ->
    [CoreStatement] ->
    [CoreStatement] ->
    Bool ->
    Maybe (Flow, Locals, Budget)
loop moduleValue budget locals condition body update testFirst
    | exhausted budget = Nothing
    | testFirst = do
        (enter, afterLocals, afterBudget) <- test budget locals
        if enter then iteration afterBudget afterLocals else Just (Proceed, afterLocals, afterBudget)
    | otherwise = iteration budget locals
    where
        test currentBudget currentLocals = case condition of
            Nothing -> Just (True, currentLocals, currentBudget)
            Just expression -> do
                (value, afterLocals, afterBudget) <- evaluate moduleValue currentBudget currentLocals expression
                selected <- truth value
                Just (selected, afterLocals, afterBudget)
        iteration currentBudget currentLocals = do
            (flow, afterBody, bodyBudget) <- executeAll moduleValue (step currentBudget) currentLocals body
            case flow of
                BreakLoop -> Just (Proceed, afterBody, bodyBudget)
                ReturnValue _ -> Just (flow, afterBody, bodyBudget)
                _ -> do
                    (updateFlow, afterUpdate, updateBudget) <- executeAll moduleValue bodyBudget afterBody update
                    case updateFlow of
                        Proceed -> loop moduleValue updateBudget afterUpdate condition body update True
                        -- A break in the update clause leaves the loop,
                        -- and a return leaves the function.
                        BreakLoop -> Just (Proceed, afterUpdate, updateBudget)
                        ReturnValue _ -> Just (updateFlow, afterUpdate, updateBudget)
                        -- The Core verifier rejects a continue placed
                        -- directly in an update clause.
                        ContinueLoop -> Nothing

store :: Int -> Value -> Locals -> Locals
store symbol value locals = (symbol, value) : filter ((/= symbol) . fst) locals

truth :: Value -> Maybe Bool
truth value = case value of
    BooleanValue flag -> Just flag
    IntegerValue number -> Just (number /= 0)
    UnitValue -> Nothing
    ClosureValue {} -> Nothing
    MemoValue {} -> Nothing
    TextValue {} -> Nothing
    FloatingValue {} -> Nothing

evaluate :: CoreModule -> Budget -> Locals -> CoreExpression -> Maybe (Value, Locals, Budget)
evaluate moduleValue budget locals expression
    | exhausted budget = Nothing
    | otherwise = case expression of
        CoreVariable name _ -> case lookup (symbolOf name) locals of
            Just value -> Just (value, locals, budget)
            -- A method named where a value is expected is a closure
            -- without captures.
            Nothing -> do
                method <-
                    firstJust
                        [ candidate
                        | candidate <- coreModuleFunctions moduleValue
                        , symbolOf (coreFunctionName candidate) == symbolOf name
                        ]
                Just
                    ( ClosureValue [] (map (symbolOf . fst) (coreFunctionParameters method)) (coreFunctionBody method)
                    , locals
                    , budget
                    )
        CoreLiteral literal valueType -> do
            value <- literalValue literal valueType
            Just (value, locals, budget)
        CoreLet name _ bound body _ -> do
            (value, afterLocals, afterBudget) <- evaluate moduleValue budget locals bound
            evaluate moduleValue afterBudget (store (symbolOf name) value afterLocals) body
        CoreConditional condition whenTrue whenFalse _ -> do
            (value, afterLocals, afterBudget) <- evaluate moduleValue budget locals condition
            selected <- truth value
            evaluate moduleValue afterBudget afterLocals (if selected then whenTrue else whenFalse)
        CorePrimitive CoreLogicalAnd [left, right] _ -> shortCircuit False left right
        CorePrimitive CoreLogicalOr [left, right] _ -> shortCircuit True left right
        -- Remembering takes a cell that no other callable has. Nothing is
        -- computed here: the callable is not called.
        CorePrimitive CoreMemoize [operand] _ -> do
            (target, afterLocals, afterBudget) <- evaluate moduleValue budget locals operand
            case target of
                ClosureValue _ [] _ ->
                    Just
                        ( MemoValue (nextCell afterBudget) target
                        , afterLocals
                        , afterBudget {nextCell = nextCell afterBudget + 1}
                        )
                _ -> Nothing
        -- A runtime call: the function its first operand names, applied
        -- to the values of the operands after it.
        CorePrimitive CoreRuntimeCall (CoreLiteral (CoreInteger identity) _ : operands) _ -> do
            function <- runtimeFunctionOf identity
            (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals operands
            (value, afterCall) <- applyRuntime function values afterBudget
            Just (value, afterLocals, afterCall)
        CorePrimitive primitive operands valueType -> do
            (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals operands
            value <- applyPrimitive primitive valueType values
            Just (value, afterLocals, afterBudget)
        CoreApply (CoreVariable callee _) arguments _
            | function : _ <-
                [ candidate
                | candidate <- coreModuleFunctions moduleValue
                , symbolOf (coreFunctionName candidate) == symbolOf callee
                ] -> do
                (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals arguments
                (value, callBudget) <- call moduleValue afterBudget function values
                Just (value, afterLocals, callBudget)
        -- The callee is evaluated before the arguments, like any operand.
        CoreApply callee arguments _ -> do
            (target, calleeLocals, calleeBudget) <- evaluate moduleValue budget locals callee
            (values, afterLocals, afterBudget) <- evaluateMany moduleValue calleeBudget calleeLocals arguments
            (value, callBudget) <- callClosure target values afterBudget
            Just (value, afterLocals, callBudget)
        CoreClosure captures parameters _ body _ -> do
            (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals (map coreCaptureValue captures)
            Just
                ( ClosureValue (zip (map (symbolOf . coreCaptureName) captures) values) (map (symbolOf . fst) parameters) body
                , afterLocals
                , afterBudget
                )
    where
        -- A closure runs on its captures and its arguments alone: it sees
        -- no local of the function that calls it.
        callClosure target values currentBudget = case target of
            ClosureValue captured parameters body
                | not (exhausted currentBudget) && length parameters == length values -> do
                    (flow, _, remaining) <-
                        executeAll moduleValue (step currentBudget) (zip parameters values ++ captured) body
                    case flow of
                        ReturnValue value -> Just (value, remaining)
                        Proceed -> Just (UnitValue, remaining)
                        _ -> Nothing
            -- The first call computes the result and keeps it in the cell;
            -- every later call, of any copy, reads the cell. A call that
            -- does not finish leaves the cell empty.
            MemoValue cell computation
                | not (exhausted currentBudget) && null values -> case lookup cell (cells currentBudget) of
                    Just remembered -> Just (remembered, step currentBudget)
                    Nothing -> do
                        (value, remaining) <- callClosure computation [] (step currentBudget)
                        Just (value, remaining {cells = (cell, value) : cells remaining})
            _ -> Nothing
        -- The right operand runs only when the left one does not decide.
        shortCircuit decidingValue left right = do
            (leftValue, afterLeft, leftBudget) <- evaluate moduleValue budget locals left
            leftTruth <- truth leftValue
            if leftTruth == decidingValue
                then Just (BooleanValue decidingValue, afterLeft, leftBudget)
                else do
                    (rightValue, afterRight, rightBudget) <- evaluate moduleValue leftBudget afterLeft right
                    rightTruth <- truth rightValue
                    Just (BooleanValue rightTruth, afterRight, rightBudget)

evaluateMany :: CoreModule -> Budget -> Locals -> [CoreExpression] -> Maybe ([Value], Locals, Budget)
evaluateMany _ budget locals [] = Just ([], locals, budget)
evaluateMany moduleValue budget locals (expression : remaining) = do
    (value, afterLocals, afterBudget) <- evaluate moduleValue budget locals expression
    (values, finalLocals, finalBudget) <- evaluateMany moduleValue afterBudget afterLocals remaining
    Just (value : values, finalLocals, finalBudget)

literalValue :: CoreLiteral -> Type -> Maybe Value
literalValue literal valueType = case literal of
    CoreInteger number
        | valueType == boolType -> Just (BooleanValue (number /= 0))
        | otherwise -> Just (IntegerValue number)
    CoreBoolean flag -> Just (BooleanValue flag)
    CoreUnit -> Just UnitValue
    CoreFloating spelling -> floatingValue valueType spelling
    CoreString value -> Just (TextValue value)
    CoreNull -> Nothing

{- | The value a floating-point literal has in its type: the decimal number
that was written, rounded to the nearest number the type holds. Only the
plain decimal spellings the tests use are read.
-}
floatingValue :: Type -> String -> Maybe Value
floatingValue valueType spelling = do
    written <- decimal (filter (/= '_') spelling)
    case valueType of
        NamedType (QualifiedName [Identifier "float"]) [] ->
            Just (FloatingValue False (toRational (fromRational written :: Double)))
        NamedType (QualifiedName [Identifier "lfloat"]) [] ->
            Just (FloatingValue False (toRational (fromRational written :: Float)))
        _ -> Nothing
    where
        decimal value =
            let (whole, afterWhole) = span isDigit value
                (fraction, afterFraction) = case afterWhole of
                    '.' : rest -> span isDigit rest
                    _ -> ("", afterWhole)
                mantissa = read ('0' : whole ++ fraction) :: Integer
                scaled = mantissa % (10 ^ length fraction)
             in case afterFraction of
                    [] | not (null whole) -> Just scaled
                    'e' : exponentText -> scaleBy scaled exponentText
                    'E' : exponentText -> scaleBy scaled exponentText
                    _ -> Nothing
        scaleBy scaled exponentText = case exponentText of
            '-' : digits | all isDigit digits, not (null digits) -> Just (scaled / (10 ^ (read digits :: Integer)))
            '+' : digits | all isDigit digits, not (null digits) -> Just (scaled * (10 ^ (read digits :: Integer)))
            digits | all isDigit digits, not (null digits) -> Just (scaled * (10 ^ (read digits :: Integer)))
            _ -> Nothing

{- | Apply a function of the runtime. The text functions are those of
"RuntimeText"; a console write adds to what the run has written.
-}
applyRuntime :: RuntimeFunction -> [Value] -> Budget -> Maybe (Value, Budget)
applyRuntime function values budget = case (function, values) of
    (TextConcat, [TextValue left, TextValue right]) -> text (left ++ right)
    (TextEquals, [TextValue left, TextValue right]) -> Just (BooleanValue (left == right), budget)
    (TextFromSigned, [IntegerValue value]) -> text (textOfSigned value)
    (TextFromUnsigned, [IntegerValue value]) -> text (textOfSigned value)
    (TextFromBool, [BooleanValue value]) -> text (textOfBool value)
    (TextFromChar, [IntegerValue value]) -> text [chr (fromInteger value)]
    (TextFormatSigned, [IntegerValue flags, IntegerValue width, IntegerValue precision, IntegerValue value]) ->
        text (formatInteger (Conversion flags width precision) value)
    (TextFormatUnsigned, [IntegerValue flags, IntegerValue width, IntegerValue precision, IntegerValue value]) ->
        text (formatInteger (Conversion flags width precision) value)
    (TextFormatFloating, [IntegerValue flags, IntegerValue width, IntegerValue precision, FloatingValue negativeZero value]) ->
        text (formatFloating (Conversion flags width precision) negativeZero value)
    (TextFormatString, [IntegerValue flags, IntegerValue width, IntegerValue precision, TextValue value]) ->
        text (formatText (Conversion flags width precision) value)
    (TextFormatChar, [IntegerValue flags, IntegerValue width, IntegerValue _, IntegerValue value]) ->
        text (formatText (Conversion flags width absent) [chr (fromInteger value)])
    (TextNewline, []) -> text lineTerminator
    (ConsoleWrite, [TextValue value, IntegerValue target])
        | target `elem` [consoleOutput, consoleOutputLine, consoleError, consoleErrorLine] ->
            let toError = target `elem` [consoleError, consoleErrorLine]
                ended = if target `elem` [consoleOutputLine, consoleErrorLine] then value ++ lineTerminator else value
             in Just (UnitValue, budget {writes = (toError, ended) : writes budget})
    _ -> Nothing
    where
        text value = Just (TextValue value, budget)

applyPrimitive :: CorePrimitive -> Type -> [Value] -> Maybe Value
applyPrimitive primitive valueType values = case (primitive, values) of
    (CoreAdd, [IntegerValue left, IntegerValue right]) -> integer (left + right)
    (CoreSubtract, [IntegerValue left, IntegerValue right]) -> integer (left - right)
    (CoreMultiply, [IntegerValue left, IntegerValue right]) -> integer (left * right)
    (CoreDivide, [IntegerValue left, IntegerValue right]) | right /= 0 -> integer (left `quot` right)
    (CoreRemainder, [IntegerValue left, IntegerValue right]) | right /= 0 -> integer (left `rem` right)
    (CoreNegate, [IntegerValue operand]) -> integer (negate operand)
    (CoreShiftLeft, [IntegerValue left, IntegerValue right]) | inShiftRange right -> integer (left `shiftL` fromInteger right)
    (CoreShiftRight, [IntegerValue left, IntegerValue right]) | inShiftRange right -> integer (left `shiftR` fromInteger right)
    (CoreBitwiseAnd, [IntegerValue left, IntegerValue right]) -> integer (left .&. right)
    (CoreBitwiseXor, [IntegerValue left, IntegerValue right]) -> integer (left `xor` right)
    (CoreBitwiseOr, [IntegerValue left, IntegerValue right]) -> integer (left .|. right)
    (CoreBitwiseNot, [IntegerValue operand]) -> integer (complement operand)
    (CoreLessThan, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left < right))
    (CoreLessEqual, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left <= right))
    (CoreGreaterThan, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left > right))
    (CoreGreaterEqual, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left >= right))
    -- On two strings the equality primitive compares the objects, which
    -- this evaluator does not model: two strings with the same characters
    -- may or may not be one object. Equal characters are the runtime
    -- function's to decide, so a program that reaches here with two
    -- strings was lowered wrongly, and it fails instead of getting the
    -- answer the native code would not give.
    (CoreEqual, [TextValue _, TextValue _]) -> Nothing
    (CoreNotEqual, [TextValue _, TextValue _]) -> Nothing
    (CoreEqual, [left, right]) -> Just (BooleanValue (left == right))
    (CoreNotEqual, [left, right]) -> Just (BooleanValue (left /= right))
    (CoreLogicalNot, [operand]) -> BooleanValue . not <$> truth operand
    -- Remembering needs the cells of the run; 'evaluate' handles it.
    (CoreMemoize, _) -> Nothing
    _ -> Nothing
    where
        -- A primitive whose result type is bool yields a Boolean even when
        -- its operands are numeric, as the logical forms of numbers do.
        integer number
            | valueType == boolType = Just (BooleanValue (number /= 0))
            | otherwise = Just (IntegerValue number)
        inShiftRange amount = amount >= 0 && amount < 64