tala-core-0.1.0: src/Progmasoft/Tala/Core/Monomorphization.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Fixed-point specialization-demand planning over verified Core.
This pass is deliberately earlier than Core optimization and CorePrep. It
proves that every parameterized type visible in the current Core module is a
valid closed specialization and records the nested demands that declaration
instantiation will consume. It does not clone declarations yet; callers must
not confuse a successful demand plan with a completed template implementation.
-}
module Progmasoft.Tala.Core.Monomorphization
( module Progmasoft.Tala.Core.Monomorphization.Types
, module Progmasoft.Tala.Core.Monomorphization.Graph
, TypeOccurrence (..)
, directTypeDependencies
, discoverTypeOccurrences
, findDemand
, findDemandByType
, planCoreMonomorphization
, planCoreMonomorphizationWith
, specializationCandidate
) 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.Monomorphization.Discovery
import Progmasoft.Tala.Core.Monomorphization.Graph
import Progmasoft.Tala.Core.Monomorphization.Types
import Progmasoft.Tala.Core.Specialization
import Progmasoft.Tala.Core.Template
data PendingDemand = PendingDemand
{ pendingType :: Type
, pendingOrigin :: DemandOrigin
, pendingDepth :: Int
}
data PlanningState = PlanningState
{ planningCatalog :: SpecializationCatalog
, planningDemands :: Map DemandId MonomorphizationDemand
, planningIdentityIds :: Map String DemandId
, planningQueue :: [PendingDemand]
, planningRootOccurrences :: Int
, planningOriginCount :: Int
, planningCacheHits :: Int
}
-- | Plan demands using 'defaultMonomorphizationLimits'.
planCoreMonomorphization :: CoreModule -> Either MonomorphizationError MonomorphizationPlan
planCoreMonomorphization = planCoreMonomorphizationWith defaultMonomorphizationLimits
-- | Plan and verify the full specialization graph under caller-supplied limits.
planCoreMonomorphizationWith ::
MonomorphizationLimits ->
CoreModule ->
Either MonomorphizationError MonomorphizationPlan
planCoreMonomorphizationWith limits coreModule = do
validateLimits limits
let occurrences = discoverTypeOccurrences coreModule
initial =
PlanningState
emptyCatalog
Map.empty
Map.empty
[PendingDemand (occurrenceType item) (RootDemand (occurrenceLocation item)) 0 | item <- occurrences]
(length occurrences)
0
0
completed <- drainQueue limits initial
let plan = finishPlan completed
graphIssues = validateDemandGraph plan
if null graphIssues
then Right plan
else Left (InvalidCompletedDemandGraph (map show graphIssues))
drainQueue :: MonomorphizationLimits -> PlanningState -> Either MonomorphizationError PlanningState
drainQueue limits state = case planningQueue state of
[] -> Right state
pending : remaining -> do
let withoutHead = state {planningQueue = remaining}
updated <- acceptDemand limits pending withoutHead
drainQueue limits updated
acceptDemand ::
MonomorphizationLimits ->
PendingDemand ->
PlanningState ->
Either MonomorphizationError PlanningState
acceptDemand limits pending state
| pendingDepth pending > maximumDemandDepth limits =
Left (DemandDepthExceeded (maximumDemandDepth limits) (pendingOrigin pending) (pendingType pending))
| otherwise = do
concrete <- mapSpecializationError pending (prepareSpecialization [] [] (pendingType pending))
let identity = renderTemplateIdentity concrete
case Map.lookup identity (planningIdentityIds state) of
Just demandId -> attachExistingOrigin limits demandId pending state
Nothing -> createDemand limits identity concrete pending state
createDemand ::
MonomorphizationLimits ->
String ->
Type ->
PendingDemand ->
PlanningState ->
Either MonomorphizationError PlanningState
createDemand limits identity concrete pending state
| Map.size (planningDemands state) >= maximumSpecializationDemands limits =
Left (SpecializationLimitExceeded (maximumSpecializationDemands limits) (pendingOrigin pending) concrete)
| planningOriginCount state >= maximumDemandOrigins limits =
Left (DemandOriginLimitExceeded (maximumDemandOrigins limits) (pendingOrigin pending) concrete)
| otherwise = do
(specialization, inserted, catalog) <-
mapSpecializationError pending (internSpecialization concrete (planningCatalog state))
if not inserted
then Left (InternalMissingDemand identity)
else do
let demandId = DemandId (specializationIdValue (specializationId specialization))
dependencies = directTypeDependencies concrete
queued =
[ PendingDemand dependency (DependencyDemand demandId argumentIndex) (pendingDepth pending + 1)
| (argumentIndex, dependency) <- dependencies
]
demand =
MonomorphizationDemand
demandId
specialization
[pendingOrigin pending]
[]
(pendingDepth pending)
DemandQueued
withDemand =
state
{ planningCatalog = catalog
, planningDemands = Map.insert demandId demand (planningDemands state)
, planningIdentityIds = Map.insert identity demandId (planningIdentityIds state)
, planningQueue = planningQueue state ++ queued
, planningOriginCount = planningOriginCount state + 1
}
pure (completeDemandDependencies demandId dependencies withDemand)
attachExistingOrigin ::
MonomorphizationLimits ->
DemandId ->
PendingDemand ->
PlanningState ->
Either MonomorphizationError PlanningState
attachExistingOrigin limits demandId pending state = case Map.lookup demandId (planningDemands state) of
Nothing -> Left (InternalMissingDemand (renderTemplateIdentity (pendingType pending)))
Just demand
| pendingOrigin pending `elem` monomorphizationOrigins demand ->
Right state {planningCacheHits = planningCacheHits state + 1}
| planningOriginCount state >= maximumDemandOrigins limits ->
Left
( DemandOriginLimitExceeded
(maximumDemandOrigins limits)
(pendingOrigin pending)
(pendingType pending)
)
| otherwise ->
let updatedDemand =
demand
{ monomorphizationOrigins = monomorphizationOrigins demand ++ [pendingOrigin pending]
, monomorphizationDepth = min (monomorphizationDepth demand) (pendingDepth pending)
}
in Right
state
{ planningDemands = Map.insert demandId updatedDemand (planningDemands state)
, planningOriginCount = planningOriginCount state + 1
, planningCacheHits = planningCacheHits state + 1
}
-- Dependency IDs may not exist when a parent is first accepted. The final
-- edge set is therefore reconstructed from canonical identities after the
-- queue reaches a fixed point. This helper only marks the parent complete
-- once its immediate children have been enqueued.
completeDemandDependencies :: DemandId -> [(Int, Type)] -> PlanningState -> PlanningState
completeDemandDependencies demandId _ state =
state
{ planningDemands =
Map.adjust
(\demand -> demand {monomorphizationState = DemandComplete})
demandId
(planningDemands state)
}
finishPlan :: PlanningState -> MonomorphizationPlan
finishPlan state =
let demands = map (resolveDependencies state) (Map.elems (planningDemands state))
edgeCount = sum (map (length . monomorphizationDependencies) demands)
deepest = foldl' max 0 (map monomorphizationDepth demands)
statistics =
MonomorphizationStatistics
(planningRootOccurrences state)
(length demands)
edgeCount
(planningCacheHits state)
deepest
in MonomorphizationPlan demands statistics
resolveDependencies :: PlanningState -> MonomorphizationDemand -> MonomorphizationDemand
resolveDependencies state demand =
demand
{ monomorphizationDependencies =
unique
[ dependencyId
| (_, dependency) <- directTypeDependencies (specializationType (monomorphizationSpecialization demand))
, let identity = renderTemplateIdentity dependency
, Just dependencyId <- [Map.lookup identity (planningIdentityIds state)]
]
}
-- | Look up a planned specialization by its stable demand identifier.
findDemand :: DemandId -> MonomorphizationPlan -> Maybe MonomorphizationDemand
findDemand demandId = lookup demandId . map pair . monomorphizationDemands
where
pair demand = (monomorphizationDemandId demand, demand)
-- | Find the canonical planned specialization matching a concrete type.
findDemandByType :: Type -> MonomorphizationPlan -> Maybe MonomorphizationDemand
findDemandByType valueType =
findByIdentity (renderTemplateIdentity valueType) . monomorphizationDemands
where
findByIdentity _ [] = Nothing
findByIdentity identity (demand : remaining)
| specializationIdentity (monomorphizationSpecialization demand) == identity = Just demand
| otherwise = findByIdentity identity remaining
validateLimits :: MonomorphizationLimits -> Either MonomorphizationError ()
validateLimits limits
| maximumSpecializationDemands limits <= 0 =
Left (InvalidMonomorphizationLimits "maximum demand count must be positive")
| maximumDemandDepth limits < 0 = Left (InvalidMonomorphizationLimits "maximum demand depth cannot be negative")
| maximumDemandOrigins limits <= 0 = Left (InvalidMonomorphizationLimits "maximum origin count must be positive")
| otherwise = Right ()
mapSpecializationError ::
PendingDemand ->
Either SpecializationError value ->
Either MonomorphizationError value
mapSpecializationError pending result = case result of
Left failure -> Left (InvalidDemandType (pendingOrigin pending) failure)
Right value -> Right value
unique :: (Eq value) => [value] -> [value]
unique = foldl' append []
where
append values value
| value `elem` values = values
| otherwise = values ++ [value]