packages feed

tala-core-0.1.0: src/Progmasoft/Tala/Core/Optimizer/Liveness.hs

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

module Progmasoft.Tala.Core.Optimizer.Liveness
    ( eliminateDeadCode
    , eliminateDeadCodeWith
    ) where

import Data.Set (Set)
import Data.Set qualified as Set
import Progmasoft.Tala.AST (ResolvedName, SymbolId, resolvedSymbol)
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.Optimizer.Analysis
import Progmasoft.Tala.Core.Optimizer.IntegerFacts
import Progmasoft.Tala.Core.Symbols (coreStatementSymbols)

-- Value liveness and declaration retention are deliberately separate. A
-- retained assignment kills the previous stored value, but it still requires
-- the CoreBind which declares its storage. Using one set for both concerns
-- either drops that declaration or falsely keeps every earlier assignment.
data LiveState = LiveState
    { liveValues :: Set SymbolId
    , requiredDeclarations :: Set SymbolId
    }

emptyLiveState :: LiveState
emptyLiveState = LiveState Set.empty Set.empty

stateForSymbols :: Set SymbolId -> LiveState
stateForSymbols symbols = LiveState symbols symbols

addRequiredSymbols :: Set SymbolId -> LiveState -> LiveState
addRequiredSymbols symbols state =
    LiveState
        { liveValues = Set.union symbols (liveValues state)
        , requiredDeclarations = Set.union symbols (requiredDeclarations state)
        }

mergeLiveStates :: [LiveState] -> LiveState
mergeLiveStates = foldl merge emptyLiveState
    where
        merge left right =
            LiveState
                { liveValues = Set.union (liveValues left) (liveValues right)
                , requiredDeclarations =
                    Set.union (requiredDeclarations left) (requiredDeclarations right)
                }

eliminateDeadCode :: CoreModule -> CoreModule
eliminateDeadCode = eliminateDeadCodeWith emptyEffectEnvironment

eliminateDeadCodeWith :: EffectEnvironment -> CoreModule -> CoreModule
eliminateDeadCodeWith environment moduleValue =
    moduleValue {coreModuleFunctions = map (eliminateFunction environment) (coreModuleFunctions moduleValue)}

eliminateFunction :: EffectEnvironment -> CoreFunction -> CoreFunction
eliminateFunction environment function =
    function
        { coreFunctionBody =
            fst (eliminateStatements environment emptyIntegerFacts emptyLiveState (coreFunctionBody function))
        }

-- Liveness runs backwards. The returned state describes storage and values
-- required before the optimized statement list executes. Function symbols
-- can occur in both sets; because they have no local CoreBind, they simply
-- flow to the function boundary and are ignored by this local pass.
eliminateStatements :: EffectEnvironment -> IntegerFacts -> LiveState -> [CoreStatement] -> ([CoreStatement], LiveState)
eliminateStatements environment incomingFacts liveAfter statements =
    foldr eliminateWithFacts ([], liveAfter) (zip (factsBeforeStatements incomingFacts statements) statements)
    where
        eliminateWithFacts (facts, statement) = eliminateOne environment facts statement

factsBeforeStatements :: IntegerFacts -> [CoreStatement] -> [IntegerFacts]
factsBeforeStatements _ [] = []
factsBeforeStatements facts (statement : remaining) =
    facts : factsBeforeStatements (transferStatementFacts facts statement) remaining

eliminateOne ::
    EffectEnvironment -> IntegerFacts -> CoreStatement -> ([CoreStatement], LiveState) -> ([CoreStatement], LiveState)
eliminateOne environment facts statement (remaining, liveAfter) = case statement of
    CoreReturn value ->
        let optimized = optimizeExpression environment value
         in ([CoreReturn optimized], stateForSymbols (expressionSymbols optimized))
    CoreEvaluate value ->
        let optimized = optimizeExpression environment value
         in if discardableExpressionWithFacts environment facts optimized
                then (remaining, liveAfter)
                else
                    ( CoreEvaluate optimized : remaining
                    , addRequiredSymbols (expressionSymbols optimized) liveAfter
                    )
    CoreBind binding -> eliminateBinding environment facts binding remaining liveAfter
    CoreAssign name value -> eliminateAssignment environment facts name value remaining liveAfter
    CoreIf condition yes no -> eliminateBranch environment facts condition yes no remaining liveAfter
    CoreWhile condition body ->
        preserveLoop (CoreWhile (optimizeExpression environment condition) body) statement remaining liveAfter
    CoreDoWhile body condition ->
        preserveLoop (CoreDoWhile body (optimizeExpression environment condition)) statement remaining liveAfter
    CoreFor condition body update ->
        preserveLoop (CoreFor (optimizeExpression environment condition) body update) statement remaining liveAfter
    CoreBreak -> (CoreBreak : remaining, liveAfter)
    CoreContinue -> (CoreContinue : remaining, liveAfter)

-- Loops have backedges, so a single reverse tree walk is not a sound liveness
-- solution. Preserve their bodies verbatim and conservatively mark every
-- symbol in the loop live until the dedicated loop fixed-point pass is added.
preserveLoop :: CoreStatement -> CoreStatement -> [CoreStatement] -> LiveState -> ([CoreStatement], LiveState)
preserveLoop optimized original remaining liveAfter =
    let symbols = Set.fromList (map (resolvedSymbol) (coreStatementSymbols original))
     in (optimized : remaining, addRequiredSymbols symbols liveAfter)

eliminateBinding ::
    EffectEnvironment -> IntegerFacts -> CoreBinding -> [CoreStatement] -> LiveState -> ([CoreStatement], LiveState)
eliminateBinding environment facts binding remaining liveAfter =
    let symbol = resolvedSymbol (coreBindingName binding)
        value = optimizeExpression environment (coreBindingValue binding)
        valueIsNeeded = Set.member symbol (liveValues liveAfter)
        declarationIsNeeded = Set.member symbol (requiredDeclarations liveAfter)
        beforeDefinition =
            LiveState
                { liveValues = Set.delete symbol (liveValues liveAfter)
                , requiredDeclarations = Set.delete symbol (requiredDeclarations liveAfter)
                }
     in if valueIsNeeded || declarationIsNeeded
            then
                ( CoreBind binding {coreBindingValue = value} : remaining
                , addRequiredSymbols (expressionSymbols value) beforeDefinition
                )
            else preserveDeadValue environment facts value remaining beforeDefinition

eliminateAssignment ::
    EffectEnvironment ->
    IntegerFacts ->
    ResolvedName ->
    CoreExpression ->
    [CoreStatement] ->
    LiveState ->
    ([CoreStatement], LiveState)
eliminateAssignment environment facts name source remaining liveAfter =
    let symbol = resolvedSymbol name
        value = optimizeExpression environment source
        beforeWrite = liveAfter {liveValues = Set.delete symbol (liveValues liveAfter)}
     in if Set.member symbol (liveValues liveAfter)
            then
                -- The write supplies the value required later, so an earlier
                -- value of the target is dead. Its declaration is different:
                -- the retained assignment still needs storage to exist.
                let withSource = addRequiredSymbols (expressionSymbols value) beforeWrite
                    withDeclaration =
                        withSource
                            { requiredDeclarations =
                                Set.insert symbol (requiredDeclarations withSource)
                            }
                 in (CoreAssign name value : remaining, withDeclaration)
            else preserveDeadValue environment facts value remaining beforeWrite

eliminateBranch ::
    EffectEnvironment ->
    IntegerFacts ->
    CoreExpression ->
    [CoreStatement] ->
    [CoreStatement] ->
    [CoreStatement] ->
    LiveState ->
    ([CoreStatement], LiveState)
eliminateBranch environment facts condition yes no remaining liveAfter =
    let optimizedCondition = optimizeExpression environment condition
        afterCondition = transferExpressionFacts facts condition
        trueFacts = refineConditionFacts True condition afterCondition
        falseFacts = refineConditionFacts False condition afterCondition
        (optimizedYes, liveYes) = eliminateStatements environment trueFacts liveAfter yes
        (optimizedNo, liveNo) = eliminateStatements environment falseFacts liveAfter no
        branchState = mergeLiveStates [liveYes, liveNo]
        liveBefore = addRequiredSymbols (expressionSymbols optimizedCondition) branchState
     in (CoreIf optimizedCondition optimizedYes optimizedNo : remaining, liveBefore)

preserveDeadValue ::
    EffectEnvironment -> IntegerFacts -> CoreExpression -> [CoreStatement] -> LiveState -> ([CoreStatement], LiveState)
preserveDeadValue environment facts value remaining liveAfter
    | discardableExpressionWithFacts environment facts value = (remaining, liveAfter)
    | otherwise =
        ( CoreEvaluate value : remaining
        , addRequiredSymbols (expressionSymbols value) liveAfter
        )

-- Closure bodies are independent liveness regions. Capture initializers are
-- evaluated outside the closure, while the body is analyzed from its own
-- return and effect roots. The module effect environment is still valid for
-- direct calls made when that callable eventually executes.
optimizeExpression :: EffectEnvironment -> CoreExpression -> CoreExpression
optimizeExpression environment expression = case expression of
    CoreVariable {} -> expression
    CoreLiteral {} -> expression
    CoreApply callee arguments valueType ->
        CoreApply
            (optimizeExpression environment callee)
            (map (optimizeExpression environment) arguments)
            valueType
    CorePrimitive primitive arguments valueType ->
        CorePrimitive primitive (map (optimizeExpression environment) arguments) valueType
    CoreLet name bindingType value body valueType ->
        CoreLet
            name
            bindingType
            (optimizeExpression environment value)
            (optimizeExpression environment body)
            valueType
    CoreConditional condition whenTrue whenFalse valueType ->
        CoreConditional
            (optimizeExpression environment condition)
            (optimizeExpression environment whenTrue)
            (optimizeExpression environment whenFalse)
            valueType
    CoreClosure captures parameters returnType body valueType ->
        CoreClosure
            [capture {coreCaptureValue = optimizeExpression environment (coreCaptureValue capture)} | capture <- captures]
            parameters
            returnType
            (fst (eliminateStatements environment emptyIntegerFacts emptyLiveState body))
            valueType