tala-frontend-0.1.0: src/Progmasoft/Tala/Closure/Analysis.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Structural closure facts shared by diagnostics, tooling, and lowering.
This module deliberately does not choose a heap layout. It describes the
semantic facts which survive into CorePrep: source order, ownership mode,
mutation, nesting, and whether a captured value is read by a nested closure.
-}
module Progmasoft.Tala.Closure.Analysis
( ClosureId (..)
, CaptureUse (..)
, ClosureSummary (..)
, ClosureCatalog (..)
, analyzeClosures
, closureById
, closuresCapturing
, stronglyCapturedSymbols
, nonOwningCapturedSymbols
) where
import Data.List (nubBy)
import Progmasoft.Tala.AST
-- | Stable preorder identity of a closure within one analyzed typed AST.
newtype ClosureId = ClosureId {closureIdValue :: Int}
deriving (Eq, Ord, Read, Show)
-- | Use and ownership facts for one closure capture.
data CaptureUse = CaptureUse
{ captureUseName :: ResolvedName
-- ^ Resolved symbol captured by the closure.
, captureUseType :: Type
-- ^ Type of the captured binding.
, captureUseMode :: CaptureMode
-- ^ Explicit or inferred ownership mode.
, captureUseOrder :: Int
-- ^ Source-order position in the capture environment.
, captureUseExplicit :: Bool
-- ^ Whether source syntax explicitly listed it.
, captureUseAlias :: Bool
-- ^ Whether the capture has an explicit alias.
, captureUseRead :: Bool
-- ^ Whether this closure reads the captured value.
, captureUseWritten :: Bool
-- ^ Whether this closure writes the captured binding.
, captureUseReadByNestedClosure :: Bool
-- ^ Whether a descendant closure reads it.
}
deriving (Eq, Ord, Read, Show)
-- | Structural properties needed to analyze or lower one closure.
data ClosureSummary = ClosureSummary
{ closureSummaryId :: ClosureId
-- ^ Unique identity assigned in source traversal order.
, closureSummaryParent :: Maybe ClosureId
-- ^ Lexically enclosing closure, if nested.
, closureSummarySpan :: SourceSpan
-- ^ Full source span of the closure expression.
, closureSummaryType :: Type
-- ^ Resolved callable type.
, closureSummaryExplicitCaptureMode :: Bool
-- ^ Whether capture policy was explicit.
, closureSummaryParameters :: [(ResolvedName, Type)]
-- ^ Resolved parameter names and types.
, closureSummaryCaptures :: [CaptureUse]
-- ^ Captures in source order.
, closureSummaryChildren :: [ClosureId]
-- ^ Directly nested closure identities.
, closureSummaryContainsReturn :: Bool
-- ^ Whether the body contains a return statement.
, closureSummaryContainsCall :: Bool
-- ^ Whether the body contains a call expression.
}
deriving (Eq, Ord, Read, Show)
-- | Closure summaries for all closures in one typed syntax tree.
newtype ClosureCatalog = ClosureCatalog {closureSummaries :: [ClosureSummary]}
deriving (Eq, Ord, Read, Show)
data WalkState = WalkState
{ walkNextId :: Int
, walkSummaries :: [ClosureSummary]
}
-- | Walk a typed AST and collect lexical nesting, captures, and use facts.
analyzeClosures :: TypedAST -> ClosureCatalog
analyzeClosures (TypedAST tree) =
let final = walkTree (WalkState 1 []) tree
in ClosureCatalog (walkSummaries final)
-- | Look up a closure summary by its traversal-assigned identity.
closureById :: ClosureId -> ClosureCatalog -> Maybe ClosureSummary
closureById identifier (ClosureCatalog summaries) =
findFirst ((== identifier) . closureSummaryId) summaries
-- | Find closures whose capture environments contain the given symbol.
closuresCapturing :: SymbolId -> ClosureCatalog -> [ClosureSummary]
closuresCapturing symbol (ClosureCatalog summaries) =
[ summary
| summary <- summaries
, any ((== symbol) . resolvedSymbol . captureUseName) (closureSummaryCaptures summary)
]
-- | Return unique symbols captured with strong ownership.
stronglyCapturedSymbols :: ClosureCatalog -> [SymbolId]
stronglyCapturedSymbols = capturedSymbolsByMode (== StrongCapture)
-- | Return unique symbols captured without strong ownership.
nonOwningCapturedSymbols :: ClosureCatalog -> [SymbolId]
nonOwningCapturedSymbols = capturedSymbolsByMode (/= StrongCapture)
capturedSymbolsByMode :: (CaptureMode -> Bool) -> ClosureCatalog -> [SymbolId]
capturedSymbolsByMode predicate (ClosureCatalog summaries) =
unique
[ resolvedSymbol (captureUseName capture)
| summary <- summaries
, capture <- closureSummaryCaptures summary
, predicate (captureUseMode capture)
]
walkTree :: WalkState -> SyntaxTree ResolvedName Type -> WalkState
walkTree state tree = foldDeclarations (syntaxDeclarations tree) state
foldDeclarations :: [Declaration ResolvedName Type] -> WalkState -> WalkState
foldDeclarations declarations initial = foldl walkDeclaration initial declarations
walkDeclaration :: WalkState -> Declaration ResolvedName Type -> WalkState
walkDeclaration state declaration = case declaration of
TypeDeclaration {typeMembers = members} -> foldl walkDeclaration state members
-- Open template members can contain closures even though they do not lower
-- before specialization. Analyze them now so tooling sees the same capture
-- relationships before and after concrete declaration instantiation.
TemplateTypeDeclaration {typeMembers = members} -> foldl walkDeclaration state members
FunctionDeclaration {declarationBody = body} -> walkBlock Nothing state body
EnumDeclaration {} -> state
walkBlock :: Maybe ClosureId -> WalkState -> Block ResolvedName Type -> WalkState
walkBlock parent state block = foldStatements parent (blockStatements block) state
foldStatements :: Maybe ClosureId -> [Statement ResolvedName Type] -> WalkState -> WalkState
foldStatements parent statements initial = foldl (walkStatement parent) initial statements
walkStatement :: Maybe ClosureId -> WalkState -> Statement ResolvedName Type -> WalkState
walkStatement parent state statement = case statement of
BindingStatement _ _ _ _ _ value -> walkExpression parent state value
AssignmentStatement _ _ _ value -> walkExpression parent state value
ReturnStatement _ value -> maybe state (walkExpression parent state) value
IfStatement _ condition trueBlock falseBlock ->
let afterCondition = walkExpression parent state condition
afterTrue = walkBlock parent afterCondition trueBlock
in maybe afterTrue (walkBlock parent afterTrue) falseBlock
WhileStatement _ condition body ->
walkBlock parent (walkExpression parent state condition) body
DoWhileStatement _ body condition ->
walkExpression parent (walkBlock parent state body) condition
ForStatement _ initializer condition updates body ->
let afterInitializer = maybe state (walkStatement parent state) initializer
afterCondition = maybe afterInitializer (walkExpression parent afterInitializer) condition
afterUpdates = foldl (walkStatement parent) afterCondition updates
in walkBlock parent afterUpdates body
ForEachStatement _ _ _ _ _ source body ->
walkBlock parent (walkExpression parent state source) body
IncrementStatement {} -> state
CompoundAssignmentStatement _ _ _ _ value -> walkExpression parent state value
DiscardStatement _ value -> walkExpression parent state value
BreakStatement _ value -> maybe state (walkExpression parent state) value
ContinueStatement {} -> state
GuardStatement _ condition block -> walkBlock parent (walkExpression parent state condition) block
BlockStatement _ block -> walkBlock parent state block
ExpressionStatement _ value _ -> walkExpression parent state value
walkExpression :: Maybe ClosureId -> WalkState -> Expression ResolvedName Type -> WalkState
walkExpression parent state expression = case expression of
NameExpression {} -> state
LiteralExpression {} -> state
MemberAccessExpression _ receiver _ _ -> walkExpression parent state receiver
MethodReferenceExpression _ receiver _ _ -> walkExpression parent state receiver
CallExpression _ callee arguments _ ->
foldl (walkExpression parent) (walkExpression parent state callee) arguments
UnaryExpression _ _ value _ -> walkExpression parent state value
BinaryExpression _ _ left right _ ->
walkExpression parent (walkExpression parent state left) right
IsPatternExpression _ subject _ _ -> walkExpression parent state subject
ConditionalExpression _ condition first second _ ->
foldl (walkExpression parent) state [condition, first, second]
CoalesceExpression _ left fallback _ ->
walkExpression parent (walkExpression parent state left) fallback
AssignmentExpression _ _ _ value _ -> walkExpression parent state value
IncrementExpression {} -> state
LoopExpression _ loop _ -> walkStatement parent state loop
BlockExpression _ block _ -> walkBlock parent state block
MatchExpression _ subjects arms _ ->
foldl (walkExpression parent) state (subjects ++ concatMap matchArmExpressions arms)
callable@CallableExpression {} -> walkCallable parent state callable
walkCallable :: Maybe ClosureId -> WalkState -> Expression ResolvedName Type -> WalkState
walkCallable parent state (CallableExpression spanValue explicit captures parameters body valueType) =
let identifier = ClosureId (walkNextId state)
bodyFacts = inspectBody body
explicitUses = zipWith (captureUse bodyFacts explicit) [0 ..] captures
implicitUses =
if explicit
then []
else inferImplicitUses parameters bodyFacts
beforeChildren = state {walkNextId = walkNextId state + 1}
afterChildren = walkCallableBody (Just identifier) beforeChildren body
children =
[ closureSummaryId candidateSummary
| candidateSummary <- walkSummaries afterChildren
, closureSummaryParent candidateSummary == Just identifier
]
nestedReads =
concatMap
(map (resolvedSymbol . captureUseName) . closureSummaryCaptures)
[ nestedSummary
| nestedSummary <- walkSummaries afterChildren
, closureSummaryId nestedSummary `elem` descendantsOf children (ClosureCatalog (walkSummaries afterChildren))
]
markNested use =
use {captureUseReadByNestedClosure = resolvedSymbol (captureUseName use) `elem` nestedReads}
summary =
ClosureSummary
{ closureSummaryId = identifier
, closureSummaryParent = parent
, closureSummarySpan = spanValue
, closureSummaryType = valueType
, closureSummaryExplicitCaptureMode = explicit
, closureSummaryParameters =
[(parameterName parameter, parameterAnnotation parameter) | parameter <- parameters]
, closureSummaryCaptures = map markNested (explicitUses ++ implicitUses)
, closureSummaryChildren = children
, closureSummaryContainsReturn = bodyContainsReturn body
, closureSummaryContainsCall = bodyContainsCall body
}
in afterChildren {walkSummaries = walkSummaries afterChildren ++ [summary]}
walkCallable _ state _ = state
walkCallableBody :: Maybe ClosureId -> WalkState -> CallableBody ResolvedName Type -> WalkState
walkCallableBody parent state body = case body of
CallableExpressionBody expression -> walkExpression parent state expression
CallableBlockBody block -> walkBlock parent state block
data BodyFacts = BodyFacts
{ factReads :: [(ResolvedName, Type)]
, factWrites :: [ResolvedName]
, factLocals :: [ResolvedName]
}
inspectBody :: CallableBody ResolvedName Type -> BodyFacts
inspectBody body = case body of
CallableExpressionBody expression -> expressionFacts expression
CallableBlockBody block -> blockFacts block
emptyFacts :: BodyFacts
emptyFacts = BodyFacts [] [] []
appendFacts :: BodyFacts -> BodyFacts -> BodyFacts
appendFacts left right =
BodyFacts
(factReads left ++ factReads right)
(factWrites left ++ factWrites right)
(factLocals left ++ factLocals right)
blockFacts :: Block ResolvedName Type -> BodyFacts
blockFacts = foldl appendFacts emptyFacts . map statementFacts . blockStatements
statementFacts :: Statement ResolvedName Type -> BodyFacts
statementFacts statement = case statement of
BindingStatement _ _ _ name _ value ->
(expressionFacts value) {factLocals = name : factLocals (expressionFacts value)}
AssignmentStatement _ name _ value ->
(expressionFacts value) {factWrites = name : factWrites (expressionFacts value)}
ReturnStatement _ value -> maybe emptyFacts expressionFacts value
IfStatement _ condition trueBlock falseBlock ->
expressionFacts condition
`appendFacts` blockFacts trueBlock
`appendFacts` maybe emptyFacts blockFacts falseBlock
WhileStatement _ condition body -> expressionFacts condition `appendFacts` blockFacts body
DoWhileStatement _ body condition -> blockFacts body `appendFacts` expressionFacts condition
ForStatement _ initializer condition updates body ->
maybe emptyFacts statementFacts initializer
`appendFacts` maybe emptyFacts expressionFacts condition
`appendFacts` foldl appendFacts emptyFacts (map statementFacts updates)
`appendFacts` blockFacts body
ForEachStatement _ _ _ name _ source body ->
let nested = expressionFacts source `appendFacts` blockFacts body
in nested {factLocals = name : factLocals nested, factWrites = name : factWrites nested}
IncrementStatement _ name annotation -> emptyFacts {factReads = [(name, annotation)], factWrites = [name]}
-- A compound assignment reads its target before storing the result.
CompoundAssignmentStatement _ _ name annotation value ->
let nested = expressionFacts value
in nested {factReads = (name, annotation) : factReads nested, factWrites = name : factWrites nested}
DiscardStatement _ value -> expressionFacts value
BreakStatement _ value -> maybe emptyFacts expressionFacts value
ContinueStatement {} -> emptyFacts
GuardStatement _ condition block -> expressionFacts condition `appendFacts` blockFacts block
BlockStatement _ block -> blockFacts block
ExpressionStatement _ value _ -> expressionFacts value
expressionFacts :: Expression ResolvedName Type -> BodyFacts
expressionFacts expression = case expression of
NameExpression _ name valueType -> BodyFacts [(name, valueType)] [] []
LiteralExpression {} -> emptyFacts
MemberAccessExpression _ receiver _ _ -> expressionFacts receiver
MethodReferenceExpression _ receiver _ _ -> expressionFacts receiver
CallExpression _ callee arguments _ ->
foldl appendFacts (expressionFacts callee) (map expressionFacts arguments)
UnaryExpression _ _ value _ -> expressionFacts value
BinaryExpression _ _ left right _ -> expressionFacts left `appendFacts` expressionFacts right
IsPatternExpression _ subject _ _ -> expressionFacts subject
ConditionalExpression _ condition first second _ ->
foldl appendFacts (expressionFacts condition) (map expressionFacts [first, second])
CoalesceExpression _ left fallback _ -> expressionFacts left `appendFacts` expressionFacts fallback
-- A simple assignment only writes its target; a compound one reads it
-- first, exactly as the statement forms do.
AssignmentExpression _ operator name value annotation ->
let nested = expressionFacts value
targetReads = maybe [] (const [(name, annotation)]) operator
in nested {factReads = targetReads ++ factReads nested, factWrites = name : factWrites nested}
IncrementExpression _ _ name annotation -> emptyFacts {factReads = [(name, annotation)], factWrites = [name]}
LoopExpression _ loop _ -> statementFacts loop
BlockExpression _ block _ -> blockFacts block
-- A pattern binding is a local of the match, written once when its arm
-- is entered.
MatchExpression _ subjects arms _ ->
let nested = foldl appendFacts emptyFacts (map expressionFacts (subjects ++ concatMap matchArmExpressions arms))
bound = [name | arm <- arms, Just name <- map matchPatternBinding (matchArmPatterns arm)]
in nested {factLocals = bound ++ factLocals nested, factWrites = bound ++ factWrites nested}
CallableExpression {} -> emptyFacts
captureUse :: BodyFacts -> Bool -> Int -> Capture ResolvedName Type -> CaptureUse
captureUse facts explicit order capture =
let symbol = resolvedSymbol (captureName capture)
isRead = any ((== symbol) . resolvedSymbol . fst) (factReads facts)
writes = any ((== symbol) . resolvedSymbol) (factWrites facts)
in CaptureUse
(captureName capture)
(captureAnnotation capture)
(captureMode capture)
order
explicit
(captureAlias capture)
isRead
writes
False
captureAlias :: Capture ResolvedName Type -> Bool
captureAlias capture = case captureInitializer capture of
Just (NameExpression _ source _) -> resolvedSymbol source /= resolvedSymbol (captureName capture)
Just _ -> True
Nothing -> False
inferImplicitUses :: [Parameter ResolvedName Type] -> BodyFacts -> [CaptureUse]
inferImplicitUses parameters facts =
let localSymbols = map (resolvedSymbol . parameterName) parameters ++ map resolvedSymbol (factLocals facts)
freeReads =
uniqueBy
(resolvedSymbol . fst)
[(name, valueType) | (name, valueType) <- factReads facts, resolvedSymbol name `notElem` localSymbols]
in [ CaptureUse
name
valueType
StrongCapture
order
False
False
True
(any ((== resolvedSymbol name) . resolvedSymbol) (factWrites facts))
False
| (order, (name, valueType)) <- zip [0 ..] freeReads
]
bodyContainsReturn :: CallableBody name annotation -> Bool
bodyContainsReturn (CallableExpressionBody _) = True
bodyContainsReturn (CallableBlockBody block) = any statementContainsReturn (blockStatements block)
statementContainsReturn :: Statement name annotation -> Bool
statementContainsReturn statement = case statement of
ReturnStatement {} -> True
IfStatement _ _ yes no ->
any statementContainsReturn (blockStatements yes)
|| maybe False (any statementContainsReturn . blockStatements) no
WhileStatement _ _ body -> any statementContainsReturn (blockStatements body)
DoWhileStatement _ body _ -> any statementContainsReturn (blockStatements body)
ForStatement _ initializer _ updates body ->
maybe False statementContainsReturn initializer
|| any statementContainsReturn updates
|| any statementContainsReturn (blockStatements body)
ForEachStatement _ _ _ _ _ _ body -> any statementContainsReturn (blockStatements body)
BreakStatement _ value -> maybe False (const False) value
GuardStatement _ _ block -> any statementContainsReturn (blockStatements block)
BlockStatement _ block -> any statementContainsReturn (blockStatements block)
-- The arms of a statement match are statements, so a return in a block
-- body returns from the enclosing callable.
ExpressionStatement _ (MatchExpression _ _ arms _) _ ->
or
[ any statementContainsReturn (blockStatements block)
| BlockExpression _ block _ <- map matchArmBody arms
]
_ -> False
bodyContainsCall :: CallableBody name annotation -> Bool
bodyContainsCall (CallableExpressionBody expression) = expressionContainsCall expression
bodyContainsCall (CallableBlockBody block) = any statementContainsCall (blockStatements block)
statementContainsCall :: Statement name annotation -> Bool
statementContainsCall statement = case statement of
BindingStatement _ _ _ _ _ value -> expressionContainsCall value
AssignmentStatement _ _ _ value -> expressionContainsCall value
ReturnStatement _ value -> maybe False expressionContainsCall value
IfStatement _ condition yes no ->
expressionContainsCall condition
|| any statementContainsCall (blockStatements yes)
|| maybe False (any statementContainsCall . blockStatements) no
WhileStatement _ condition body ->
expressionContainsCall condition || any statementContainsCall (blockStatements body)
DoWhileStatement _ body condition ->
any statementContainsCall (blockStatements body) || expressionContainsCall condition
ForStatement _ initializer condition updates body ->
maybe False statementContainsCall initializer
|| maybe False expressionContainsCall condition
|| any statementContainsCall updates
|| any statementContainsCall (blockStatements body)
ForEachStatement _ _ _ _ _ source body ->
expressionContainsCall source || any statementContainsCall (blockStatements body)
IncrementStatement {} -> False
CompoundAssignmentStatement _ _ _ _ value -> expressionContainsCall value
DiscardStatement _ value -> expressionContainsCall value
BreakStatement _ value -> maybe False expressionContainsCall value
ContinueStatement {} -> False
GuardStatement _ condition block ->
expressionContainsCall condition || any statementContainsCall (blockStatements block)
BlockStatement _ block -> any statementContainsCall (blockStatements block)
ExpressionStatement _ value _ -> expressionContainsCall value
expressionContainsCall :: Expression name annotation -> Bool
expressionContainsCall expression = case expression of
CallExpression {} -> True
MemberAccessExpression _ receiver _ _ -> expressionContainsCall receiver
MethodReferenceExpression _ receiver _ _ -> expressionContainsCall receiver
UnaryExpression _ _ value _ -> expressionContainsCall value
BinaryExpression _ _ left right _ -> expressionContainsCall left || expressionContainsCall right
IsPatternExpression _ subject _ _ -> expressionContainsCall subject
ConditionalExpression _ condition first second _ -> any expressionContainsCall [condition, first, second]
CoalesceExpression _ left fallback _ -> expressionContainsCall left || expressionContainsCall fallback
AssignmentExpression _ _ _ value _ -> expressionContainsCall value
IncrementExpression {} -> False
LoopExpression _ loop _ -> statementContainsCall loop
BlockExpression _ block _ -> any statementContainsCall (blockStatements block)
MatchExpression _ subjects arms _ ->
any expressionContainsCall (subjects ++ concatMap matchArmExpressions arms)
CallableExpression {} -> False
NameExpression {} -> False
LiteralExpression {} -> False
descendantsOf :: [ClosureId] -> ClosureCatalog -> [ClosureId]
descendantsOf roots catalog = roots ++ concatMap children roots
where
children identifier = case closureById identifier catalog of
Nothing -> []
Just summary -> descendantsOf (closureSummaryChildren summary) catalog
findFirst :: (value -> Bool) -> [value] -> Maybe value
findFirst _ [] = Nothing
findFirst predicate (value : remaining)
| predicate value = Just value
| otherwise = findFirst predicate remaining
unique :: (Eq value) => [value] -> [value]
unique = nubBy (==)
uniqueBy :: (Eq key) => (value -> key) -> [value] -> [value]
uniqueBy select = nubBy (\left right -> select left == select right)