tala-core-0.1.0: src/Progmasoft/Tala/Core/Optimizer/Inline.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Capture-avoiding linear-body inlining for verified Core.
Pure, non-recursive functions may contain immutable bindings and evaluations
before their single final return. The pass converts that straight-line body to
nested 'CoreLet' expressions. Non-trivial arguments are let-bound too, so they
are evaluated exactly once even when the callee reads a parameter repeatedly
or not at all. Every copied binder receives a fresh module-wide SymbolId.
-}
module Progmasoft.Tala.Core.Optimizer.Inline
( InlineReport (..)
, inlineFunctions
) where
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.Optimizer.Analysis
import Progmasoft.Tala.Core.Symbols (nextCoreSymbolValue)
data InlineStep
= InlineBinding ResolvedName Type CoreExpression
| InlineEvaluation CoreExpression
deriving (Eq, Ord, Read, Show)
data InlineCandidate = InlineCandidate
{ candidateParameters :: [(ResolvedName, Type)]
, candidateSteps :: [InlineStep]
, candidateResult :: CoreExpression
, candidateExpressionNodes :: Int
}
deriving (Eq, Ord, Read, Show)
data CandidateDiscovery = CandidateDiscovery
{ discoveredCandidates :: Map SymbolId InlineCandidate
, discoveredExpressionCandidates :: Int
, discoveredStatementCandidates :: Int
, rejectedNonLinearBodies :: Int
}
-- | Counts describing candidate discovery and capture-avoiding rewrites.
data InlineReport = InlineReport
{ inlineCandidateCount :: Int
, inlineExpressionCandidateCount :: Int
, inlineStatementCandidateCount :: Int
, inlineRejectedNonLinearBodies :: Int
, inlineRewrittenCalls :: Int
, inlineSkippedOversized :: Int
, inlineGeneratedParameterLets :: Int
, inlineGeneratedLocalLets :: Int
, inlineGeneratedEvaluationLets :: Int
, inlineAlphaRenamedSymbols :: Int
}
deriving (Eq, Ord, Read, Show)
data InlineState = InlineState
{ stateNextSymbol :: Int
, stateRewrittenCalls :: Int
, stateSkippedOversized :: Int
, stateGeneratedParameterLets :: Int
, stateGeneratedLocalLets :: Int
, stateGeneratedEvaluationLets :: Int
, stateAlphaRenamedSymbols :: Int
}
initialInlineState :: CoreModule -> InlineState
initialInlineState moduleValue =
InlineState (nextCoreSymbolValue moduleValue) 0 0 0 0 0 0
inlineFunctions :: Int -> [FunctionEffectReport] -> CoreModule -> (CoreModule, InlineReport)
inlineFunctions maximumNodes effectReports moduleValue =
let discovery = discoverCandidates effectReports moduleValue
candidates = discoveredCandidates discovery
(functions, finalState) =
rewriteFunctions maximumNodes candidates (initialInlineState moduleValue) (coreModuleFunctions moduleValue)
in ( moduleValue {coreModuleFunctions = functions}
, InlineReport
{ inlineCandidateCount = Map.size candidates
, inlineExpressionCandidateCount = discoveredExpressionCandidates discovery
, inlineStatementCandidateCount = discoveredStatementCandidates discovery
, inlineRejectedNonLinearBodies = rejectedNonLinearBodies discovery
, inlineRewrittenCalls = stateRewrittenCalls finalState
, inlineSkippedOversized = stateSkippedOversized finalState
, inlineGeneratedParameterLets = stateGeneratedParameterLets finalState
, inlineGeneratedLocalLets = stateGeneratedLocalLets finalState
, inlineGeneratedEvaluationLets = stateGeneratedEvaluationLets finalState
, inlineAlphaRenamedSymbols = stateAlphaRenamedSymbols finalState
}
)
discoverCandidates :: [FunctionEffectReport] -> CoreModule -> CandidateDiscovery
discoverCandidates reports moduleValue = foldl' discover emptyDiscovery (coreModuleFunctions moduleValue)
where
reportMap = Map.fromList [(resolvedSymbol (effectFunctionName report), report) | report <- reports]
emptyDiscovery = CandidateDiscovery Map.empty 0 0 0
discover discovery function =
let symbol = resolvedSymbol (coreFunctionName function)
in case Map.lookup symbol reportMap of
Just report
| effectClassification report == PureEffect
, not (effectIsRecursive report) ->
case linearizeBody (coreFunctionBody function) of
Just (steps, result) ->
let candidate =
InlineCandidate
(coreFunctionParameters function)
steps
result
(linearizedNodeCount (length (coreFunctionParameters function)) steps result)
statementDelta = if null steps then 0 else 1
in discovery
{ discoveredCandidates = Map.insert symbol candidate (discoveredCandidates discovery)
, discoveredExpressionCandidates = discoveredExpressionCandidates discovery + 1 - statementDelta
, discoveredStatementCandidates = discoveredStatementCandidates discovery + statementDelta
}
Nothing -> discovery {rejectedNonLinearBodies = rejectedNonLinearBodies discovery + 1}
_ -> discovery
-- Only a final return is accepted. Mutable state, branches, early returns, and
-- fallthrough retain their call boundary until a dedicated CFG inliner exists.
linearizeBody :: [CoreStatement] -> Maybe ([InlineStep], CoreExpression)
linearizeBody = go []
where
go steps [CoreReturn result] = Just (reverse steps, result)
go steps (CoreBind binding : remaining)
| not (coreBindingMutable binding) =
go (InlineBinding (coreBindingName binding) (coreBindingType binding) (coreBindingValue binding) : steps) remaining
go steps (CoreEvaluate value : remaining) = go (InlineEvaluation value : steps) remaining
go _ _ = Nothing
rewriteFunctions ::
Int -> Map SymbolId InlineCandidate -> InlineState -> [CoreFunction] -> ([CoreFunction], InlineState)
rewriteFunctions maximumNodes candidates = mapAccumulating rewriteFunction
where
rewriteFunction state function =
let (body, next) = rewriteStatements maximumNodes candidates state (coreFunctionBody function)
in (function {coreFunctionBody = body}, next)
mapAccumulating :: (state -> value -> (result, state)) -> state -> [value] -> ([result], state)
mapAccumulating _ state [] = ([], state)
mapAccumulating transform state (value : remaining) =
let (result, afterValue) = transform state value
(results, finalState) = mapAccumulating transform afterValue remaining
in (result : results, finalState)
rewriteStatements ::
Int -> Map SymbolId InlineCandidate -> InlineState -> [CoreStatement] -> ([CoreStatement], InlineState)
rewriteStatements maximumNodes candidates = mapAccumulating rewrite
where
rewrite state statement = case statement of
CoreBind binding ->
let (value, next) = rewriteExpression maximumNodes candidates state (coreBindingValue binding)
in (CoreBind binding {coreBindingValue = value}, next)
CoreAssign name value ->
let (rewritten, next) = rewriteExpression maximumNodes candidates state value
in (CoreAssign name rewritten, next)
CoreReturn value ->
let (rewritten, next) = rewriteExpression maximumNodes candidates state value
in (CoreReturn rewritten, next)
CoreEvaluate value ->
let (rewritten, next) = rewriteExpression maximumNodes candidates state value
in (CoreEvaluate rewritten, next)
CoreIf condition yes no ->
let (rewrittenCondition, afterCondition) = rewriteExpression maximumNodes candidates state condition
(rewrittenYes, afterYes) = rewriteStatements maximumNodes candidates afterCondition yes
(rewrittenNo, finalState) = rewriteStatements maximumNodes candidates afterYes no
in (CoreIf rewrittenCondition rewrittenYes rewrittenNo, finalState)
CoreWhile condition body ->
let (rewrittenCondition, afterCondition) = rewriteExpression maximumNodes candidates state condition
(rewrittenBody, finalState) = rewriteStatements maximumNodes candidates afterCondition body
in (CoreWhile rewrittenCondition rewrittenBody, finalState)
CoreDoWhile body condition ->
let (rewrittenBody, afterBody) = rewriteStatements maximumNodes candidates state body
(rewrittenCondition, finalState) = rewriteExpression maximumNodes candidates afterBody condition
in (CoreDoWhile rewrittenBody rewrittenCondition, finalState)
CoreFor condition body update ->
let (rewrittenCondition, afterCondition) = rewriteExpression maximumNodes candidates state condition
(rewrittenBody, afterBody) = rewriteStatements maximumNodes candidates afterCondition body
(rewrittenUpdate, finalState) = rewriteStatements maximumNodes candidates afterBody update
in (CoreFor rewrittenCondition rewrittenBody rewrittenUpdate, finalState)
CoreBreak -> (CoreBreak, state)
CoreContinue -> (CoreContinue, state)
rewriteExpression ::
Int -> Map SymbolId InlineCandidate -> InlineState -> CoreExpression -> (CoreExpression, InlineState)
rewriteExpression maximumNodes candidates state expression = case expression of
CoreVariable {} -> (expression, state)
CoreLiteral {} -> (expression, state)
CorePrimitive primitive arguments valueType ->
let (rewritten, next) = mapAccumulating (rewriteExpression maximumNodes candidates) state arguments
in (CorePrimitive primitive rewritten valueType, next)
CoreLet name bindingType value body valueType ->
let (rewrittenValue, afterValue) = rewriteExpression maximumNodes candidates state value
(rewrittenBody, afterBody) = rewriteExpression maximumNodes candidates afterValue body
in (CoreLet name bindingType rewrittenValue rewrittenBody valueType, afterBody)
-- Inlined bodies stay nested inside the arm that contained the call, so
-- an unselected arm still evaluates nothing.
CoreConditional condition whenTrue whenFalse valueType ->
let (rewrittenCondition, afterCondition) = rewriteExpression maximumNodes candidates state condition
(rewrittenTrue, afterTrue) = rewriteExpression maximumNodes candidates afterCondition whenTrue
(rewrittenFalse, afterFalse) = rewriteExpression maximumNodes candidates afterTrue whenFalse
in (CoreConditional rewrittenCondition rewrittenTrue rewrittenFalse valueType, afterFalse)
CoreClosure captures parameters returnType body valueType ->
let (rewrittenCaptures, afterCaptures) = mapAccumulating rewriteCapture state captures
(rewrittenBody, finalState) = rewriteStatements maximumNodes candidates afterCaptures body
in (CoreClosure rewrittenCaptures parameters returnType rewrittenBody valueType, finalState)
where
rewriteCapture current capture =
let (value, next) = rewriteExpression maximumNodes candidates current (coreCaptureValue capture)
in (capture {coreCaptureValue = value}, next)
CoreApply callee arguments valueType ->
let (rewrittenCallee, afterCallee) = rewriteExpression maximumNodes candidates state callee
(rewrittenArguments, afterArguments) = mapAccumulating (rewriteExpression maximumNodes candidates) afterCallee arguments
rebuilt = CoreApply rewrittenCallee rewrittenArguments valueType
in tryInline maximumNodes candidates afterArguments rebuilt
tryInline :: Int -> Map SymbolId InlineCandidate -> InlineState -> CoreExpression -> (CoreExpression, InlineState)
tryInline maximumNodes candidates state call@(CoreApply callee arguments valueType) =
case calleeSymbol callee >>= (`Map.lookup` candidates) of
Nothing -> (call, state)
Just candidate
| length (candidateParameters candidate) /= length arguments -> (call, state)
| candidateExpressionNodes candidate > normalizedMaximum -> skipped
| otherwise ->
let (expanded, afterExpansion) = expandCandidate candidate arguments state
in if expressionNodeCount expanded > normalizedMaximum || expressionType expanded /= valueType
then skipped
else (expanded, afterExpansion {stateRewrittenCalls = stateRewrittenCalls afterExpansion + 1})
where
normalizedMaximum = max 1 maximumNodes
skipped = (call, state {stateSkippedOversized = stateSkippedOversized state + 1})
tryInline _ _ state expression = (expression, state)
calleeSymbol :: CoreExpression -> Maybe SymbolId
calleeSymbol (CoreVariable name _) = Just (resolvedSymbol name)
calleeSymbol _ = Nothing
-- A substitution environment carries complete expressions. Variables and
-- literals can be copied safely; every other argument first receives a let.
expandCandidate :: InlineCandidate -> [CoreExpression] -> InlineState -> (CoreExpression, InlineState)
expandCandidate candidate arguments initialState =
let (environment, parameterLets, afterParameters) =
prepareParameters (candidateParameters candidate) arguments initialState
(body, afterBody) = cloneLinearBody environment (candidateSteps candidate) (candidateResult candidate) afterParameters
in (foldr applyLet body parameterLets, afterBody)
where
applyLet (name, valueType, value) body = CoreLet name valueType value body (expressionType body)
type Substitution = Map SymbolId CoreExpression
prepareParameters ::
[(ResolvedName, Type)] ->
[CoreExpression] ->
InlineState ->
(Substitution, [(ResolvedName, Type, CoreExpression)], InlineState)
prepareParameters parameters arguments = go Map.empty [] (zip parameters arguments)
where
go environment bindings [] state = (environment, reverse bindings, state)
go environment bindings (((parameter, parameterType), argument) : remaining) state
| trivialArgument argument =
go (Map.insert (resolvedSymbol parameter) argument environment) bindings remaining state
| otherwise =
let (fresh, next) = freshName "argument" parameter state
reference = CoreVariable fresh parameterType
counted = next {stateGeneratedParameterLets = stateGeneratedParameterLets next + 1}
in go
(Map.insert (resolvedSymbol parameter) reference environment)
((fresh, parameterType, argument) : bindings)
remaining
counted
trivialArgument :: CoreExpression -> Bool
trivialArgument CoreVariable {} = True
trivialArgument CoreLiteral {} = True
trivialArgument _ = False
cloneLinearBody :: Substitution -> [InlineStep] -> CoreExpression -> InlineState -> (CoreExpression, InlineState)
cloneLinearBody initialEnvironment steps result initialState =
let (environment, builders, afterSteps) = cloneSteps initialEnvironment [] steps initialState
(clonedResult, afterResult) = cloneExpression environment result afterSteps
in (foldr ($) clonedResult builders, afterResult)
cloneSteps ::
Substitution ->
[CoreExpression -> CoreExpression] ->
[InlineStep] ->
InlineState ->
(Substitution, [CoreExpression -> CoreExpression], InlineState)
cloneSteps environment builders [] state = (environment, reverse builders, state)
cloneSteps environment builders (step : remaining) state = case step of
InlineBinding oldName valueType value ->
let (clonedValue, afterValue) = cloneExpression environment value state
(fresh, afterName) = freshName "local" oldName afterValue
reference = CoreVariable fresh valueType
environment' = Map.insert (resolvedSymbol oldName) reference environment
builder body = CoreLet fresh valueType clonedValue body (expressionType body)
counted = afterName {stateGeneratedLocalLets = stateGeneratedLocalLets afterName + 1}
in cloneSteps environment' (builder : builders) remaining counted
InlineEvaluation value ->
let (clonedValue, afterValue) = cloneExpression environment value state
(fresh, afterName) = freshSynthetic "evaluation" afterValue
builder body = CoreLet fresh (expressionType clonedValue) clonedValue body (expressionType body)
counted = afterName {stateGeneratedEvaluationLets = stateGeneratedEvaluationLets afterName + 1}
in cloneSteps environment (builder : builders) remaining counted
cloneExpression :: Substitution -> CoreExpression -> InlineState -> (CoreExpression, InlineState)
cloneExpression environment expression state = case expression of
CoreVariable name _ -> (Map.findWithDefault expression (resolvedSymbol name) environment, state)
CoreLiteral {} -> (expression, state)
CoreApply callee arguments valueType ->
let (clonedCallee, afterCallee) = cloneExpression environment callee state
(clonedArguments, afterArguments) =
mapAccumulating (\current value -> cloneExpression environment value current) afterCallee arguments
in (CoreApply clonedCallee clonedArguments valueType, afterArguments)
CorePrimitive primitive arguments valueType ->
let (clonedArguments, afterArguments) =
mapAccumulating (\current value -> cloneExpression environment value current) state arguments
in (CorePrimitive primitive clonedArguments valueType, afterArguments)
CoreLet oldName bindingType value body valueType ->
let (clonedValue, afterValue) = cloneExpression environment value state
(fresh, afterName) = freshName "let" oldName afterValue
bodyEnvironment = Map.insert (resolvedSymbol oldName) (CoreVariable fresh bindingType) environment
(clonedBody, afterBody) = cloneExpression bodyEnvironment body afterName
in (CoreLet fresh bindingType clonedValue clonedBody valueType, afterBody)
CoreConditional condition whenTrue whenFalse valueType ->
let (clonedCondition, afterCondition) = cloneExpression environment condition state
(clonedTrue, afterTrue) = cloneExpression environment whenTrue afterCondition
(clonedFalse, afterFalse) = cloneExpression environment whenFalse afterTrue
in (CoreConditional clonedCondition clonedTrue clonedFalse valueType, afterFalse)
CoreClosure captures parameters returnType body valueType ->
let (clonedValues, afterValues) = mapAccumulating cloneCaptureValue state captures
(captureEnvironment, clonedCaptures, afterCaptures) = cloneCaptureNames environment clonedValues afterValues
(closureEnvironment, clonedParameters, afterParameters) = cloneNames "closure-parameter" captureEnvironment parameters afterCaptures
(clonedBody, _, afterBody) = cloneStatements closureEnvironment body afterParameters
in (CoreClosure clonedCaptures clonedParameters returnType clonedBody valueType, afterBody)
where
cloneCaptureValue current capture =
let (value, next) = cloneExpression environment (coreCaptureValue capture) current
in (capture {coreCaptureValue = value}, next)
cloneCaptureNames :: Substitution -> [CoreCapture] -> InlineState -> (Substitution, [CoreCapture], InlineState)
cloneCaptureNames = go []
where
go changed environment [] state = (environment, reverse changed, state)
go changed environment (capture : remaining) state =
let oldName = coreCaptureName capture
valueType = coreCaptureType capture
(fresh, next) = freshName "capture" oldName state
environment' = Map.insert (resolvedSymbol oldName) (CoreVariable fresh valueType) environment
in go (capture {coreCaptureName = fresh} : changed) environment' remaining next
cloneNames ::
String -> Substitution -> [(ResolvedName, Type)] -> InlineState -> (Substitution, [(ResolvedName, Type)], InlineState)
cloneNames role = go []
where
go changed environment [] state = (environment, reverse changed, state)
go changed environment ((oldName, valueType) : remaining) state =
let (fresh, next) = freshName role oldName state
environment' = Map.insert (resolvedSymbol oldName) (CoreVariable fresh valueType) environment
in go ((fresh, valueType) : changed) environment' remaining next
cloneStatements :: Substitution -> [CoreStatement] -> InlineState -> ([CoreStatement], Substitution, InlineState)
cloneStatements environment [] state = ([], environment, state)
cloneStatements environment (statement : remaining) state =
let (cloned, nextEnvironment, afterStatement) = cloneStatement environment statement state
(rest, finalEnvironment, finalState) = cloneStatements nextEnvironment remaining afterStatement
in (cloned : rest, finalEnvironment, finalState)
cloneStatement :: Substitution -> CoreStatement -> InlineState -> (CoreStatement, Substitution, InlineState)
cloneStatement environment statement state = case statement of
CoreBind binding ->
let (value, afterValue) = cloneExpression environment (coreBindingValue binding) state
oldName = coreBindingName binding
valueType = coreBindingType binding
(fresh, afterName) = freshName "closure-local" oldName afterValue
changed = binding {coreBindingName = fresh, coreBindingValue = value}
environment' = Map.insert (resolvedSymbol oldName) (CoreVariable fresh valueType) environment
in (CoreBind changed, environment', afterName)
CoreAssign oldName value ->
let (clonedValue, next) = cloneExpression environment value state
changedName = case Map.lookup (resolvedSymbol oldName) environment of
Just (CoreVariable name _) -> name
_ -> oldName
in (CoreAssign changedName clonedValue, environment, next)
CoreReturn value ->
let (clonedValue, next) = cloneExpression environment value state
in (CoreReturn clonedValue, environment, next)
CoreEvaluate value ->
let (clonedValue, next) = cloneExpression environment value state
in (CoreEvaluate clonedValue, environment, next)
CoreIf condition whenTrue whenFalse ->
let (clonedCondition, afterCondition) = cloneExpression environment condition state
(clonedTrue, _, afterTrue) = cloneStatements environment whenTrue afterCondition
(clonedFalse, _, afterFalse) = cloneStatements environment whenFalse afterTrue
in (CoreIf clonedCondition clonedTrue clonedFalse, environment, afterFalse)
CoreWhile condition body ->
let (clonedCondition, afterCondition) = cloneExpression environment condition state
(clonedBody, _, finalState) = cloneStatements environment body afterCondition
in (CoreWhile clonedCondition clonedBody, environment, finalState)
CoreDoWhile body condition ->
let (clonedBody, _, afterBody) = cloneStatements environment body state
(clonedCondition, finalState) = cloneExpression environment condition afterBody
in (CoreDoWhile clonedBody clonedCondition, environment, finalState)
CoreFor condition body update ->
let (clonedCondition, afterCondition) = cloneExpression environment condition state
(clonedBody, _, afterBody) = cloneStatements environment body afterCondition
(clonedUpdate, _, finalState) = cloneStatements environment update afterBody
in (CoreFor clonedCondition clonedBody clonedUpdate, environment, finalState)
CoreBreak -> (CoreBreak, environment, state)
CoreContinue -> (CoreContinue, environment, state)
freshName :: String -> ResolvedName -> InlineState -> (ResolvedName, InlineState)
freshName role original state =
let value = stateNextSymbol state
Identifier spelling = resolvedSpelling original
fresh = ResolvedName (SymbolId value) (Identifier ("$inline." ++ role ++ "." ++ spelling ++ "." ++ show value))
in (fresh, state {stateNextSymbol = value + 1, stateAlphaRenamedSymbols = stateAlphaRenamedSymbols state + 1})
freshSynthetic :: String -> InlineState -> (ResolvedName, InlineState)
freshSynthetic role state = freshName role (ResolvedName (SymbolId 1) (Identifier role)) state
linearizedNodeCount :: Int -> [InlineStep] -> CoreExpression -> Int
linearizedNodeCount parameterCount steps result = parameterCount + sum (map stepNodeCount steps) + expressionNodeCount result
where
stepNodeCount step = case step of
InlineBinding _ _ value -> 1 + expressionNodeCount value
InlineEvaluation value -> 1 + expressionNodeCount value
expressionNodeCount :: CoreExpression -> Int
expressionNodeCount expression = case expression of
CoreVariable {} -> 1
CoreLiteral {} -> 1
CoreApply callee arguments _ -> 1 + sum (map expressionNodeCount (callee : arguments))
CorePrimitive _ arguments _ -> 1 + sum (map expressionNodeCount arguments)
CoreLet _ _ value body _ -> 1 + expressionNodeCount value + expressionNodeCount body
CoreConditional condition whenTrue whenFalse _ ->
1 + sum (map expressionNodeCount [condition, whenTrue, whenFalse])
CoreClosure captures _ _ body _ -> 1 + sum (map (expressionNodeCount . coreCaptureValue) captures) + sum (map statementNodeCount body)
statementNodeCount :: CoreStatement -> Int
statementNodeCount statement = case statement of
CoreBind binding -> 1 + expressionNodeCount (coreBindingValue binding)
CoreAssign _ value -> 1 + expressionNodeCount value
CoreReturn value -> 1 + expressionNodeCount value
CoreEvaluate value -> 1 + expressionNodeCount value
CoreIf condition yes no -> 1 + expressionNodeCount condition + sum (map statementNodeCount (yes ++ no))
CoreWhile condition body -> 1 + expressionNodeCount condition + sum (map statementNodeCount body)
CoreDoWhile body condition -> 1 + sum (map statementNodeCount body) + expressionNodeCount condition
CoreFor condition body update -> 1 + expressionNodeCount condition + sum (map statementNodeCount (body ++ update))
CoreBreak -> 1
CoreContinue -> 1