packages feed

tala-frontend-0.1.0: src/Progmasoft/Tala/Template/Application.hs

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

{- |
Typed template declaration catalog and application binding.

The parser and semantic passes preserve open declarations.  This module owns
the next boundary: matching a concrete ordered argument list to one template
declaration without cloning its body yet.  Keeping binding independent from
Core is important because defaults and packs are source declaration semantics,
whereas Core may only contain a fully closed specialization.
-}
module Progmasoft.Tala.Template.Application
    ( TemplateCatalog (..)
    , TemplateDeclarationDescriptor (..)
    , TemplateParameterDescriptor (..)
    , TemplateParameterCategory (..)
    , TemplateApplication (..)
    , TemplateBinding (..)
    , BoundTemplateArgument (..)
    , TemplateApplicationError (..)
    , buildTemplateCatalog
    , lookupTemplateDeclaration
    , bindTemplateApplication
    , minimumTemplateArity
    , maximumTemplateArity
    , substituteType
    , substituteTemplateArgument
    , substituteTemplateValue
    , renderTemplateApplicationError
    ) where

import Progmasoft.Tala.AST

-- | Index of all typed template declarations in one compilation unit.
newtype TemplateCatalog = TemplateCatalog
    { templateCatalogDeclarations :: [TemplateDeclarationDescriptor]
    -- ^ Declarations retained in deterministic source order.
    }
    deriving (Eq, Ord, Read, Show)

-- | Resolved template declaration identity, parameters, and member signatures.
data TemplateDeclarationDescriptor = TemplateDeclarationDescriptor
    { templateDeclarationName :: QualifiedName
    -- ^ Fully qualified source name.
    , templateDeclarationSymbol :: SymbolId
    -- ^ Unique symbol assigned by the renamer.
    , templateDeclarationParameters :: [TemplateParameterDescriptor]
    -- ^ Parameters in declaration order.
    , templateDeclarationMembers :: [(ResolvedName, Type)]
    -- ^ Direct members and their resolved types.
    }
    deriving (Eq, Ord, Read, Show)

-- | Semantic matching requirements for one template parameter.
data TemplateParameterDescriptor = TemplateParameterDescriptor
    { templateDescriptorName :: ResolvedName
    -- ^ Resolved parameter identity.
    , templateDescriptorCategory :: TemplateParameterCategory
    -- ^ Accepted argument category and shape.
    , templateDescriptorIsPack :: Bool
    -- ^ Whether the parameter captures remaining arguments.
    , templateDescriptorDefault :: Maybe TemplateDefault
    -- ^ Source default, if declared.
    , templateDescriptorAnnotation :: Type
    -- ^ Resolved value/type annotation.
    }
    deriving (Eq, Ord, Read, Show)

-- | Category expected for an explicit template argument.
data TemplateParameterCategory
    = -- | A type-valued template parameter.
      TypeParameterCategory
    | -- | A compile-time value constrained by this type.
      ValueParameterCategory Type
    | -- | A template-template parameter with accepted shape.
      TemplateParameterCategory [TemplateParameterShape]
    deriving (Eq, Ord, Read, Show)

-- | One application request with an ordered list of supplied arguments.
data TemplateApplication = TemplateApplication
    { templateApplicationTarget :: QualifiedName
    -- ^ Fully qualified declaration to instantiate.
    , templateApplicationArguments :: [TemplateArgument]
    -- ^ Explicit arguments in source order.
    }
    deriving (Eq, Ord, Read, Show)

-- | Complete parameter-to-argument environment after defaults and packs bind.
data TemplateBinding = TemplateBinding
    { templateBindingDeclaration :: TemplateDeclarationDescriptor
    -- ^ Declaration selected by name lookup.
    , templateBindingArguments :: [(ResolvedName, [BoundTemplateArgument])]
    -- ^ Bound values grouped by parameter identity.
    }
    deriving (Eq, Ord, Read, Show)

-- | Argument together with whether it was supplied or came from a default.
data BoundTemplateArgument
    = -- | Written by the source application.
      ExplicitTemplateArgument TemplateArgument
    | -- | Filled from the parameter declaration.
      DefaultTemplateArgument TemplateArgument
    deriving (Eq, Ord, Read, Show)

-- | Binding, lookup, or substitution failure for a template application.
data TemplateApplicationError
    = -- | No declaration has this qualified name.
      UnknownTemplateDeclaration QualifiedName
    | -- | Required arity and supplied arity.
      TooFewTemplateArguments QualifiedName Int Int
    | -- | Maximum arity and supplied arity.
      TooManyTemplateArguments QualifiedName Int Int
    | -- | An argument does not match its parameter category.
      TemplateArgumentCategoryMismatch QualifiedName ResolvedName Int TemplateParameterCategory TemplateArgument
    | -- | A default references a parameter without a bound value.
      UnresolvedTemplateDefault QualifiedName ResolvedName TemplateDefault
    | -- | A parameter pack default cannot be represented.
      UnsupportedTemplatePackDefault QualifiedName ResolvedName
    | -- | Multiple declarations share the requested name.
      AmbiguousTemplateDeclaration QualifiedName [SymbolId]
    deriving (Eq, Ord, Read, Show)

-- | Build a source-ordered catalog from template declarations in a typed AST.
buildTemplateCatalog :: TypedAST -> TemplateCatalog
buildTemplateCatalog (TypedAST (SyntaxTree namespace declarations)) =
    TemplateCatalog (concatMap (describe namespace) declarations)
    where
        describe owner declaration = case declaration of
            TemplateTypeDeclaration _ name _ parameters members ->
                [ TemplateDeclarationDescriptor
                    (qualify owner (resolvedSpelling name))
                    (resolvedSymbol name)
                    (map describeParameter parameters)
                    [(declarationName member, declarationAnnotation member) | member <- members]
                ]
            _ -> []

        qualify Nothing name = QualifiedName [name]
        qualify (Just (QualifiedName parts)) name = QualifiedName (parts ++ [name])

describeParameter :: TemplateParameter ResolvedName Type -> TemplateParameterDescriptor
describeParameter parameter =
    TemplateParameterDescriptor
        (templateParameterName parameter)
        category
        (templateParameterIsPack parameter)
        (templateParameterDefault parameter)
        (templateParameterAnnotation parameter)
    where
        category = case templateParameterKind parameter of
            TemplateTypeParameter -> TypeParameterCategory
            TemplateValueParameterKind _ -> ValueParameterCategory (templateParameterAnnotation parameter)
            TemplateTemplateParameter shapes -> TemplateParameterCategory shapes

-- | Resolve a qualified template name, rejecting missing or ambiguous matches.
lookupTemplateDeclaration ::
    QualifiedName ->
    TemplateCatalog ->
    Either TemplateApplicationError TemplateDeclarationDescriptor
lookupTemplateDeclaration name catalog = case matching of
    [] -> Left (UnknownTemplateDeclaration name)
    [declaration] -> Right declaration
    declarations -> Left (AmbiguousTemplateDeclaration name (map templateDeclarationSymbol declarations))
    where
        matching = filter ((== name) . templateDeclarationName) (templateCatalogDeclarations catalog)

-- | Count required explicit arguments after defaults and packs are considered.
minimumTemplateArity :: TemplateDeclarationDescriptor -> Int
minimumTemplateArity = length . filter required . templateDeclarationParameters
    where
        required parameter =
            not (templateDescriptorIsPack parameter)
                && templateDescriptorDefault parameter == Nothing

-- | Return the finite maximum argument count, or Nothing for a parameter pack.
maximumTemplateArity :: TemplateDeclarationDescriptor -> Maybe Int
maximumTemplateArity declaration
    | any templateDescriptorIsPack parameters = Nothing
    | otherwise = Just (length parameters)
    where
        parameters = templateDeclarationParameters declaration

-- | Bind explicit arguments, defaults, and packs to the selected declaration.
bindTemplateApplication ::
    TemplateCatalog ->
    TemplateApplication ->
    Either [TemplateApplicationError] TemplateBinding
bindTemplateApplication catalog application = case lookupTemplateDeclaration target catalog of
    Left issue -> Left [issue]
    Right declaration ->
        let supplied = templateApplicationArguments application
            minimumArity = minimumTemplateArity declaration
            maximumArity = maximumTemplateArity declaration
         in if length supplied < minimumArity
                then Left [TooFewTemplateArguments target minimumArity (length supplied)]
                else case maximumArity of
                    Just limit
                        | length supplied > limit ->
                            Left [TooManyTemplateArguments target limit (length supplied)]
                    _ -> bindKnownDeclaration declaration supplied
    where
        target = templateApplicationTarget application

bindKnownDeclaration ::
    TemplateDeclarationDescriptor ->
    [TemplateArgument] ->
    Either [TemplateApplicationError] TemplateBinding
bindKnownDeclaration declaration supplied =
    case bindParameters declaration [] (templateDeclarationParameters declaration) supplied of
        (bindings, [], []) -> Right (TemplateBinding declaration bindings)
        (_, leftovers, []) ->
            Left
                [ TooManyTemplateArguments
                    (templateDeclarationName declaration)
                    (length supplied - length leftovers)
                    (length supplied)
                ]
        (_, _, problems) -> Left problems

type PartialBindings = [(ResolvedName, [BoundTemplateArgument])]

bindParameters ::
    TemplateDeclarationDescriptor ->
    PartialBindings ->
    [TemplateParameterDescriptor] ->
    [TemplateArgument] ->
    (PartialBindings, [TemplateArgument], [TemplateApplicationError])
bindParameters _ bindings [] supplied = (bindings, supplied, [])
bindParameters declaration bindings (parameter : remaining) supplied
    | templateDescriptorIsPack parameter =
        let reserve = minimumRequired remaining
            count = max 0 (length supplied - reserve)
            (packArguments, rest) = splitAt count supplied
            checked = zipWith (validateCategory declaration parameter) [bindingOffset bindings ..] packArguments
            problems = concatMap snd checked
            values = map (ExplicitTemplateArgument . fst) checked
            nextBindings = bindings ++ [(templateDescriptorName parameter, values)]
            (finalBindings, leftovers, laterProblems) =
                bindParameters declaration nextBindings remaining rest
         in (finalBindings, leftovers, problems ++ laterProblems)
    | otherwise = case supplied of
        argument : rest ->
            let (checked, problems) = validateCategory declaration parameter (bindingOffset bindings) argument
                nextBindings = bindings ++ [(templateDescriptorName parameter, [ExplicitTemplateArgument checked])]
                (finalBindings, leftovers, laterProblems) =
                    bindParameters declaration nextBindings remaining rest
             in (finalBindings, leftovers, problems ++ laterProblems)
        [] -> case templateDescriptorDefault parameter of
            Nothing ->
                ( bindings
                , []
                ,
                    [ TooFewTemplateArguments
                        (templateDeclarationName declaration)
                        (minimumTemplateArity declaration)
                        (bindingOffset bindings)
                    ]
                )
            Just defaultSyntax -> case resolveDefaultFor declaration bindings [resolvedSymbol (templateDescriptorName parameter)] defaultSyntax of
                Nothing ->
                    ( bindings
                    , []
                    ,
                        [ UnresolvedTemplateDefault
                            (templateDeclarationName declaration)
                            (templateDescriptorName parameter)
                            defaultSyntax
                        ]
                    )
                Just argument ->
                    let (checked, problems) = validateCategory declaration parameter (bindingOffset bindings) argument
                        nextBindings = bindings ++ [(templateDescriptorName parameter, [DefaultTemplateArgument checked])]
                        (finalBindings, leftovers, laterProblems) =
                            bindParameters declaration nextBindings remaining []
                     in (finalBindings, leftovers, problems ++ laterProblems)

minimumRequired :: [TemplateParameterDescriptor] -> Int
minimumRequired = length . filter required
    where
        required parameter =
            not (templateDescriptorIsPack parameter)
                && templateDescriptorDefault parameter == Nothing

bindingOffset :: PartialBindings -> Int
bindingOffset = sum . map (length . snd)

validateCategory ::
    TemplateDeclarationDescriptor ->
    TemplateParameterDescriptor ->
    Int ->
    TemplateArgument ->
    (TemplateArgument, [TemplateApplicationError])
validateCategory declaration parameter index argument
    | acceptsCategory (templateDescriptorCategory parameter) argument = (argument, [])
    | otherwise =
        ( argument
        ,
            [ TemplateArgumentCategoryMismatch
                (templateDeclarationName declaration)
                (templateDescriptorName parameter)
                index
                (templateDescriptorCategory parameter)
                argument
            ]
        )

acceptsCategory :: TemplateParameterCategory -> TemplateArgument -> Bool
acceptsCategory category argument = case (category, argument) of
    (TypeParameterCategory, TypeTemplateArgument _) -> True
    (ValueParameterCategory _, ValueTemplateArgument _) -> True
    -- A template-template argument is represented by its named type until the
    -- declaration catalog grows first-class template references.  Requiring a
    -- zero-argument named type prevents values and structural function types
    -- from being mistaken for declaration identities.
    (TemplateParameterCategory _, TypeTemplateArgument (NamedType _ [])) -> True
    _ -> False

resolveDefault :: PartialBindings -> TemplateDefault -> Maybe TemplateArgument
resolveDefault bindings defaultSyntax = case defaultSyntax of
    TemplateTypeDefault valueType -> TypeTemplateArgument <$> resolveDefaultType bindings valueType
    TemplateValueDefault value -> ValueTemplateArgument <$> resolveDefaultValue bindings value

-- Defaults may name a parameter declared later in the list.  Resolution is
-- therefore graph-shaped rather than a left fold.  The visited SymbolId set
-- rejects a default cycle without relying on source order or recursion depth.
resolveDefaultFor ::
    TemplateDeclarationDescriptor ->
    PartialBindings ->
    [SymbolId] ->
    TemplateDefault ->
    Maybe TemplateArgument
resolveDefaultFor declaration bindings visited defaultSyntax = case defaultSyntax of
    TemplateTypeDefault (ExplicitType identifier) ->
        case lookupBoundType identifier bindings of
            Just valueType -> Just (TypeTemplateArgument valueType)
            Nothing -> case findParameter identifier declaration of
                Nothing -> resolveDefault bindings defaultSyntax
                Just parameter ->
                    let symbol = resolvedSymbol (templateDescriptorName parameter)
                     in if symbol `elem` visited
                            then Nothing
                            else do
                                nestedDefault <- templateDescriptorDefault parameter
                                resolveDefaultFor declaration bindings (symbol : visited) nestedDefault
    TemplateValueDefault (TemplateNameSyntax _ (QualifiedName [identifier])) ->
        case lookupBoundValue identifier bindings of
            Just value -> Just (ValueTemplateArgument value)
            Nothing -> do
                parameter <- findParameter identifier declaration
                let symbol = resolvedSymbol (templateDescriptorName parameter)
                if symbol `elem` visited
                    then Nothing
                    else do
                        nestedDefault <- templateDescriptorDefault parameter
                        resolveDefaultFor declaration bindings (symbol : visited) nestedDefault
    _ -> resolveDefault bindings defaultSyntax

findParameter :: Identifier -> TemplateDeclarationDescriptor -> Maybe TemplateParameterDescriptor
findParameter identifier declaration = first matching (templateDeclarationParameters declaration)
    where
        matching parameter = resolvedSpelling (templateDescriptorName parameter) == identifier
        first _ [] = Nothing
        first predicate (value : remaining)
            | predicate value = Just value
            | otherwise = first predicate remaining

resolveDefaultType :: PartialBindings -> TypeSyntax -> Maybe Type
resolveDefaultType bindings syntax = case syntax of
    AutoType -> Nothing
    ExplicitType identifier ->
        lookupBoundType identifier bindings
            `orElse` Just (NamedType (QualifiedName [identifier]) [])
    QualifiedTypeSyntax name arguments ->
        NamedType name <$> traverse (resolveDefaultArgument bindings) arguments
    BuiltinArrayTypeSyntax element ->
        named "[]" . (: []) . TypeTemplateArgument <$> resolveDefaultType bindings element
    ArrayTypeSyntax element ->
        system "Array" . (: []) . TypeTemplateArgument <$> resolveDefaultType bindings element
    FixedArrayTypeSyntax element size -> do
        elementType <- resolveDefaultType bindings element
        sizeValue <- resolveDefaultValue bindings size
        pure (system "Array" [TypeTemplateArgument elementType, ValueTemplateArgument sizeValue])
    DictionaryTypeSyntax key value -> do
        keyType <- resolveDefaultType bindings key
        valueType <- resolveDefaultType bindings value
        pure (system "Dictionary" [TypeTemplateArgument keyType, TypeTemplateArgument valueType])
    CallableTypeSyntax parameters result ->
        FunctionType <$> traverse (resolveDefaultType bindings) parameters <*> resolveDefaultType bindings result
    where
        named spelling arguments = NamedType (QualifiedName [Identifier spelling]) arguments
        system spelling arguments = NamedType (QualifiedName [Identifier "System", Identifier spelling]) arguments

resolveDefaultArgument :: PartialBindings -> TemplateArgumentSyntax -> Maybe TemplateArgument
resolveDefaultArgument bindings argument = case argument of
    TemplateTypeSyntax valueType -> TypeTemplateArgument <$> resolveDefaultType bindings valueType
    TemplateValueArgumentSyntax value -> ValueTemplateArgument <$> resolveDefaultValue bindings value

resolveDefaultValue :: PartialBindings -> TemplateValueSyntax -> Maybe TemplateValue
resolveDefaultValue bindings syntax = case syntax of
    TemplateIntegerSyntax _ value -> Just (IntegerTemplateValue value)
    TemplateCharacterSyntax _ value -> Just (CharacterTemplateValue value)
    TemplateBooleanSyntax _ value -> Just (BooleanTemplateValue value)
    TemplateNameSyntax _ (QualifiedName [identifier]) -> lookupBoundValue identifier bindings
    -- Arithmetic defaults are deliberately not evaluated here.  The type
    -- checker already validates them, and the specialization evaluator will
    -- own substitution-aware constant folding in the cloning stage.
    TemplateUnarySyntax {} -> Nothing
    TemplateBinarySyntax {} -> Nothing
    TemplateNameSyntax {} -> Nothing

lookupBoundType :: Identifier -> PartialBindings -> Maybe Type
lookupBoundType identifier bindings = do
    arguments <- lookupBySpelling identifier bindings
    case arguments of
        [ExplicitTemplateArgument (TypeTemplateArgument valueType)] -> Just valueType
        [DefaultTemplateArgument (TypeTemplateArgument valueType)] -> Just valueType
        _ -> Nothing

lookupBoundValue :: Identifier -> PartialBindings -> Maybe TemplateValue
lookupBoundValue identifier bindings = do
    arguments <- lookupBySpelling identifier bindings
    case arguments of
        [ExplicitTemplateArgument (ValueTemplateArgument value)] -> Just value
        [DefaultTemplateArgument (ValueTemplateArgument value)] -> Just value
        _ -> Nothing

lookupBySpelling :: Identifier -> PartialBindings -> Maybe [BoundTemplateArgument]
lookupBySpelling identifier = lookupFirst . filter ((== identifier) . resolvedSpelling . fst)
    where
        lookupFirst [] = Nothing
        lookupFirst ((_, values) : _) = Just values

orElse :: Maybe a -> Maybe a -> Maybe a
orElse (Just value) _ = Just value
orElse Nothing fallback = fallback

-- | Replace bound type variables recursively in a resolved type expression.
substituteType :: TemplateBinding -> Type -> Either TemplateApplicationError Type
substituteType binding valueType = case valueType of
    TypeVariable name -> case lookup (resolvedSymbol name) substitutions of
        Just [TypeTemplateArgument replacement] -> Right replacement
        _ -> Left (missing name (TemplateTypeDefault (ExplicitType (resolvedSpelling name))))
    NamedType name arguments -> NamedType name <$> traverse (substituteTemplateArgument binding) arguments
    FunctionType parameters result -> FunctionType <$> traverse (substituteType binding) parameters <*> substituteType binding result
    ErrorType -> Right ErrorType
    where
        substitutions = flattenedBindings binding
        missing name defaultValue =
            UnresolvedTemplateDefault
                (templateDeclarationName (templateBindingDeclaration binding))
                name
                defaultValue

-- | Substitute type or compile-time value variables inside one argument.
substituteTemplateArgument :: TemplateBinding -> TemplateArgument -> Either TemplateApplicationError TemplateArgument
substituteTemplateArgument binding argument = case argument of
    TypeTemplateArgument valueType -> TypeTemplateArgument <$> substituteType binding valueType
    ValueTemplateArgument value -> ValueTemplateArgument <$> substituteTemplateValue binding value

-- | Substitute one value-parameter reference using the completed binding.
substituteTemplateValue :: TemplateBinding -> TemplateValue -> Either TemplateApplicationError TemplateValue
substituteTemplateValue binding value = case value of
    TemplateValueParameter name -> case lookup (resolvedSymbol name) (flattenedBindings binding) of
        Just [ValueTemplateArgument replacement] -> Right replacement
        _ ->
            Left
                ( UnresolvedTemplateDefault
                    (templateDeclarationName (templateBindingDeclaration binding))
                    name
                    (TemplateValueDefault (TemplateNameSyntax syntheticSpan (QualifiedName [resolvedSpelling name])))
                )
    _ -> Right value

flattenedBindings :: TemplateBinding -> [(SymbolId, [TemplateArgument])]
flattenedBindings binding =
    [ (resolvedSymbol name, map unwrap values)
    | (name, values) <- templateBindingArguments binding
    ]
    where
        unwrap (ExplicitTemplateArgument argument) = argument
        unwrap (DefaultTemplateArgument argument) = argument

syntheticSpan :: SourceSpan
syntheticSpan = SourceSpan "<template>" (SourcePosition 1 1) (SourcePosition 1 1)

-- | Render a template lookup, arity, category, or substitution failure.
renderTemplateApplicationError :: TemplateApplicationError -> String
renderTemplateApplicationError issue = case issue of
    UnknownTemplateDeclaration name -> "unknown template declaration " ++ renderQualifiedName name
    TooFewTemplateArguments name expected actual ->
        renderQualifiedName name
            ++ " requires at least "
            ++ show expected
            ++ " template arguments, but received "
            ++ show actual
    TooManyTemplateArguments name expected actual ->
        renderQualifiedName name
            ++ " accepts at most "
            ++ show expected
            ++ " template arguments, but received "
            ++ show actual
    TemplateArgumentCategoryMismatch name parameter index category argument ->
        "template argument "
            ++ show (index + 1)
            ++ " for "
            ++ renderQualifiedName name
            ++ " does not match parameter "
            ++ identifierText (resolvedSpelling parameter)
            ++ " (expected "
            ++ renderCategory category
            ++ ", received "
            ++ renderArgumentCategory argument
            ++ ")"
    UnresolvedTemplateDefault name parameter _ ->
        "default for template parameter "
            ++ identifierText (resolvedSpelling parameter)
            ++ " of "
            ++ renderQualifiedName name
            ++ " could not be resolved"
    UnsupportedTemplatePackDefault name parameter ->
        "template parameter pack "
            ++ identifierText (resolvedSpelling parameter)
            ++ " of "
            ++ renderQualifiedName name
            ++ " cannot have a default"
    AmbiguousTemplateDeclaration name symbols ->
        "template declaration "
            ++ renderQualifiedName name
            ++ " is ambiguous across symbols "
            ++ show (map symbolIdValue symbols)

renderQualifiedName :: QualifiedName -> String
renderQualifiedName (QualifiedName parts) = joinWith "." (map identifierText parts)

renderCategory :: TemplateParameterCategory -> String
renderCategory category = case category of
    TypeParameterCategory -> "type"
    ValueParameterCategory _ -> "value"
    TemplateParameterCategory _ -> "template"

renderArgumentCategory :: TemplateArgument -> String
renderArgumentCategory argument = case argument of
    TypeTemplateArgument _ -> "type"
    ValueTemplateArgument _ -> "value"

joinWith :: String -> [String] -> String
joinWith _ [] = ""
joinWith _ [value] = value
joinWith separator (value : remaining) = value ++ separator ++ joinWith separator remaining