tala-compiler-0.1.0: src/Progmasoft/Tala/Compiler.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Composition of the target-independent Tala frontend stages.
A physical source file is a compilation unit, not a module. Namespace is the
semantic grouping key and does not need to match the directory layout. The
project compiler therefore parses every file independently, merges units by
namespace, runs each namespace through the complete Haskell pipeline, and
selects the configured namespace/class entry only after type checking.
-}
module Progmasoft.Tala.Compiler
( CompilerInput (..)
, FrontendArtifacts (..)
, NamespaceArtifacts (..)
, ProjectFrontendArtifacts (..)
, compileToCorePrep
, compileEntryToCorePrep
, compileProjectToCorePrep
, compileTemplateSpecializations
, projectEntryCore
, validateEntryPoint
) where
import Data.List (groupBy, sortOn)
import Data.Maybe (mapMaybe)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.CorePrep
import Progmasoft.Tala.Core.CorePrep.Verifier
import Progmasoft.Tala.Core.Monomorphization
import Progmasoft.Tala.Core.Optimizer
import Progmasoft.Tala.Core.Verifier
import Progmasoft.Tala.Desugarer
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.Frontend
import Progmasoft.Tala.Template.Discovery
import Progmasoft.Tala.Template.Specialization
import Progmasoft.Tala.Template.Specialization.Verifier
{- | Artifacts for one semantic namespace after all of its physical source
units have been merged. Keeping every stage visible makes stage ownership and
regression tests explicit; CorePrep remains internal at the process boundary.
-}
data FrontendArtifacts = FrontendArtifacts
{ artifactParsedAST :: ParsedAST
, artifactRenamedAST :: RenamedAST
, artifactResolvedAST :: ResolvedAST
, artifactTypedAST :: TypedAST
, artifactTemplateDemandDiscovery :: TemplateDemandDiscovery
, artifactTemplateSpecializations :: TemplateSpecializationPlan
, artifactCore :: CoreModule
, artifactMonomorphizationPlan :: MonomorphizationPlan
, artifactOptimizedCore :: CoreModule
, artifactCorePrep :: CorePrepModule
}
deriving (Eq, Ord, Read, Show)
-- | Frontend stages and source provenance for one merged semantic namespace.
data NamespaceArtifacts = NamespaceArtifacts
{ artifactNamespace :: Maybe QualifiedName
, artifactSourceFiles :: [FilePath]
, artifactFrontend :: FrontendArtifacts
}
deriving (Eq, Ord, Read, Show)
{- | A project may contain several namespaces, but the current private Core wire
carries one module at a time. All namespaces are still parsed, renamed,
resolved, typed, lowered, optimized, and verified. The selected entry
namespace is the module handed to the native CorePrep/Xpp/Xmm boundary.
-}
data ProjectFrontendArtifacts = ProjectFrontendArtifacts
{ projectEntryName :: QualifiedName
, projectNamespaces :: [NamespaceArtifacts]
, projectEntryNamespace :: NamespaceArtifacts
}
deriving (Eq, Ord, Read, Show)
-- | Run the source pipeline through verified, optimized CorePrep for one file.
compileToCorePrep :: CompilerInput -> Either [Diagnostic] FrontendArtifacts
compileToCorePrep input = analyzeSemantics input >>= compileSemanticToCorePrep
-- | Compile one source file and validate its namespace-qualified entry class.
compileEntryToCorePrep :: QualifiedName -> CompilerInput -> Either [Diagnostic] FrontendArtifacts
compileEntryToCorePrep entry input = do
artifacts <- compileToCorePrep input
validateEntryPoint entry (artifactTypedAST artifacts)
pure artifacts
{- | Materialize an explicit batch of resolved template demands and lower the
resulting closed declaration view to verified Core. Demand discovery and
constraint ordering stay outside this function: callers must pass demands
produced by semantic resolution rather than asking the driver to infer syntax.
-}
compileTemplateSpecializations ::
TemplateSpecializationLimits ->
TypedAST ->
[TemplateSpecializationDemand] ->
Either [Diagnostic] (TemplateSpecializationPlan, CoreModule)
compileTemplateSpecializations limits typed demands = do
plan <-
mapLeft templateSpecializationDiagnostics (planTemplateSpecializations limits typed demands)
>>= mapLeft templatePlanDiagnostics . verifyTemplateSpecializationPlan
core <- runDesugarer defaultDesugarer (specializationTypedAST plan) >>= verifyCore
pure (plan, core)
{- | Compile a complete discovered source set. Diagnostics from independent
files and namespaces are accumulated in deterministic input order instead of
stopping at the first bad file, which makes project output useful without
weakening any stage boundary.
-}
compileProjectToCorePrep :: QualifiedName -> [CompilerInput] -> Either [Diagnostic] ProjectFrontendArtifacts
compileProjectToCorePrep entry inputs = do
namespaceName <- entryNamespaceName entry
parsedUnits <- collectResults (map parseUnit inputs)
if null parsedUnits
then Left [compilerProblem "TAE0010" Nothing "project source set is empty"]
else do
compiled <- collectResults (map compileNamespace (groupParsedUnits parsedUnits))
selected <- selectNamespace namespaceName compiled
validateEntryPoint entry (artifactTypedAST (artifactFrontend selected))
pure (ProjectFrontendArtifacts entry compiled selected)
where
parseUnit input = do
syntax <- analyzeSyntax input
pure (compilerSourceFile input, syntaxParsedAST syntax)
-- | Retrieve the selected namespace's optimized Core module for native lowering.
projectEntryCore :: ProjectFrontendArtifacts -> CoreModule
projectEntryCore = artifactOptimizedCore . artifactFrontend . projectEntryNamespace
compileParsedToCorePrep :: ParsedAST -> Either [Diagnostic] FrontendArtifacts
compileParsedToCorePrep parsed = analyzeParsedSemantics parsed >>= compileSemanticToCorePrep
compileSemanticToCorePrep :: SemanticArtifacts -> Either [Diagnostic] FrontendArtifacts
compileSemanticToCorePrep semantic = do
let parsed = semanticParsedAST semantic
renamed = semanticRenamedAST semantic
resolved = semanticResolvedAST semantic
typed = semanticTypedAST semantic
templateDiscovery = discoverTemplateDemands typed
templatePlan <-
mapLeft
templateSpecializationDiagnostics
(planTemplateSpecializations defaultTemplateSpecializationLimits typed (discoveredTemplateDemands templateDiscovery))
>>= mapLeft templatePlanDiagnostics . verifyTemplateSpecializationPlan
-- The two trees are one program: which calls have an effect is
-- decided from both.
let specializations = specializationTypedAST templatePlan
desugarer = desugarerWithin [typed, specializations]
ordinaryCore <- runDesugarer desugarer typed >>= verifyCore
specializationCore <- runDesugarer desugarer specializations >>= verifyCore
let core =
CoreModuleWithSources
(coreModuleName ordinaryCore)
(coreModuleFunctions ordinaryCore ++ coreModuleFunctions specializationCore)
(unique (coreModuleSourceFiles ordinaryCore ++ coreModuleSourceFiles specializationCore))
(coreModuleFunctionSources ordinaryCore ++ coreModuleFunctionSources specializationCore)
verifiedCore <- verifyCore core
-- Demand discovery runs before optimization so dead-code elimination
-- cannot silently erase a type required by the checked source contract.
-- The declaration planner consumes this discovery immediately. The later
-- Core graph remains separate because it describes backend work rather
-- than source declaration selection.
specializationPlan <- mapLeft monomorphizationDiagnostic (planCoreMonomorphization verifiedCore)
optimized <- runCoreOptimizer defaultCoreOptimizer verifiedCore >>= verifyCore
prepared <- prepareCore optimized >>= verifyCorePrep
pure
( FrontendArtifacts
parsed
renamed
resolved
typed
templateDiscovery
templatePlan
verifiedCore
specializationPlan
optimized
prepared
)
mapLeft :: (failure -> mapped) -> Either failure value -> Either mapped value
mapLeft transform result = case result of
Left failure -> Left (transform failure)
Right value -> Right value
monomorphizationDiagnostic :: MonomorphizationError -> [Diagnostic]
monomorphizationDiagnostic failure =
[ Diagnostic
CoreStage
Error
"TAM1001"
Nothing
(renderMonomorphizationError failure)
]
templateSpecializationDiagnostics :: [TemplateSpecializationError] -> [Diagnostic]
templateSpecializationDiagnostics = zipWith diagnostic [1 :: Int ..]
where
diagnostic index failure =
Diagnostic
TypeCheckerStage
Error
("TAT2" ++ pad index)
Nothing
(renderTemplateSpecializationError failure)
pad value
| value < 10 = "00" ++ show value
| value < 100 = "0" ++ show value
| otherwise = show value
templatePlanDiagnostics :: [TemplatePlanIssue] -> [Diagnostic]
templatePlanDiagnostics = zipWith diagnostic [1 :: Int ..]
where
diagnostic index failure =
Diagnostic
TypeCheckerStage
Error
("TAT3" ++ pad index)
Nothing
(renderTemplatePlanIssue failure)
pad value
| value < 10 = "00" ++ show value
| value < 100 = "0" ++ show value
| otherwise = show value
data ParsedNamespace = ParsedNamespace
{ parsedNamespaceName :: Maybe QualifiedName
, parsedNamespaceFiles :: [FilePath]
, parsedNamespaceDeclarations :: [Declaration Identifier ()]
}
groupParsedUnits :: [(FilePath, ParsedAST)] -> [ParsedNamespace]
groupParsedUnits units = map mergeGroup grouped
where
keyed = sortOn namespaceSortKey [(syntaxNamespace tree, file, syntaxDeclarations tree) | (file, ParsedAST tree) <- units]
grouped = groupBy sameNamespace keyed
sameNamespace (left, _, _) (right, _, _) = left == right
mergeGroup [] = ParsedNamespace Nothing [] []
mergeGroup values@((namespaceName, _, _) : _) =
ParsedNamespace
namespaceName
[file | (_, file, _) <- values]
(concat [declarations | (_, _, declarations) <- values])
namespaceSortKey :: (Maybe QualifiedName, FilePath, declarations) -> (Maybe QualifiedName, FilePath)
namespaceSortKey (namespaceName, file, _) = (namespaceName, file)
compileNamespace :: ParsedNamespace -> Either [Diagnostic] NamespaceArtifacts
compileNamespace parsed = do
let syntaxTree = SyntaxTree (parsedNamespaceName parsed) (parsedNamespaceDeclarations parsed)
artifacts <- compileParsedToCorePrep (ParsedAST syntaxTree)
let frontend = artifacts
sourceFiles = unique (map portablePath (parsedNamespaceFiles parsed))
attachSources core = core {coreModuleSourceFiles = unique (sourceFiles ++ coreModuleSourceFiles core)}
prepared =
(artifactCorePrep frontend)
{ corePrepModuleSourceFiles = unique (sourceFiles ++ corePrepModuleSourceFiles (artifactCorePrep frontend))
}
updatedFrontend =
frontend
{ artifactCore = attachSources (artifactCore frontend)
, artifactOptimizedCore = attachSources (artifactOptimizedCore frontend)
, artifactCorePrep = prepared
}
_ <- verifyCore (artifactOptimizedCore updatedFrontend)
_ <- verifyCorePrep prepared
pure
( NamespaceArtifacts
(parsedNamespaceName parsed)
sourceFiles
updatedFrontend
)
portablePath :: FilePath -> FilePath
portablePath = map (\character -> if character == '\\' then '/' else character)
selectNamespace :: QualifiedName -> [NamespaceArtifacts] -> Either [Diagnostic] NamespaceArtifacts
selectNamespace namespaceName artifacts =
case [candidate | candidate <- artifacts, artifactNamespace candidate == Just namespaceName] of
[selected] -> Right selected
[] -> Left [compilerProblem "TAE0011" Nothing ("entry namespace was not found: " ++ renderQualifiedName namespaceName)]
_ ->
Left
[ compilerProblem "TAE0012" Nothing ("entry namespace was produced more than once: " ++ renderQualifiedName namespaceName)
]
-- | Check that an entry path names a public static parameterless void Main method.
validateEntryPoint :: QualifiedName -> TypedAST -> Either [Diagnostic] ()
validateEntryPoint entry (TypedAST (SyntaxTree namespace declarations)) = do
expectedNamespace <- entryNamespaceName entry
className <- entryClassName entry
if namespace /= Just expectedNamespace
then Left [entryProblem "TAE0002" Nothing "entry namespace does not match the selected source namespace"]
else case matchingTypes className declarations of
[] -> Left [entryProblem "TAE0003" Nothing ("entry class was not found: " ++ identifierText className)]
[declaration] -> validateEntryMethod declaration
_ -> Left [entryProblem "TAE0013" Nothing ("entry class is ambiguous: " ++ identifierText className)]
matchingTypes :: Identifier -> [Declaration ResolvedName Type] -> [Declaration ResolvedName Type]
matchingTypes className declarations =
[ declaration
| declaration@TypeDeclaration {} <- declarations
, resolvedSpelling (declarationName declaration) == className
]
validateEntryMethod :: Declaration ResolvedName Type -> Either [Diagnostic] ()
validateEntryMethod TypeDeclaration {typeMembers = members} =
case matchingMethods of
[] -> Left [entryProblem "TAE0004" Nothing "entry class must declare Main"]
[ method@FunctionDeclaration
{ declarationAccess = access
, declarationIsStatic = isStatic
, declarationParameters = parameters
, declarationReturnSyntax = returnSyntax
}
]
| access /= PublicAccess -> failure method "TAE0005" "entry Main must be public"
| not isStatic -> failure method "TAE0006" "entry Main must be static"
| not (null parameters) -> failure method "TAE0007" "entry Main must not declare parameters"
| not (syntaxSpellsVoid returnSyntax) ->
failure method "TAE0008" "entry Main must return void"
| otherwise -> Right ()
_ -> Left [entryProblem "TAE0014" (declarationSpan <$> firstMethod) "entry class must declare exactly one Main method"]
where
matchingMethods =
[ member
| member@FunctionDeclaration {} <- members
, identifierText (resolvedSpelling (declarationName member)) == "Main"
]
firstMethod = case matchingMethods of
[] -> Nothing
method : _ -> Just method
failure method code message = Left [entryProblem code (Just (declarationSpan method)) message]
validateEntryMethod _ = Left [entryProblem "TAE0009" Nothing "entry target is not a class"]
syntaxSpellsVoid :: TypeSyntax -> Bool
syntaxSpellsVoid syntax = case syntax of
ExplicitType (Identifier "void") -> True
QualifiedTypeSyntax (QualifiedName [Identifier "void"]) [] -> True
_ -> False
entryNamespaceName :: QualifiedName -> Either [Diagnostic] QualifiedName
entryNamespaceName (QualifiedName parts)
| length parts < 2 = Left [entryProblem "TAE0001" Nothing "entry must name a namespace-qualified class"]
| otherwise = Right (QualifiedName (init parts))
entryClassName :: QualifiedName -> Either [Diagnostic] Identifier
entryClassName (QualifiedName parts) =
case reverse parts of
className : _ : _ -> Right className
_ -> Left [entryProblem "TAE0001" Nothing "entry must name a namespace-qualified class"]
renderQualifiedName :: QualifiedName -> String
renderQualifiedName (QualifiedName parts) = joinWithDot (map identifierText parts)
joinWithDot :: [String] -> String
joinWithDot [] = ""
joinWithDot [value] = value
joinWithDot (value : remaining) = value ++ "." ++ joinWithDot remaining
unique :: (Eq value) => [value] -> [value]
unique = foldl add []
where
add values value
| value `elem` values = values
| otherwise = values ++ [value]
collectResults :: [Either [Diagnostic] value] -> Either [Diagnostic] [value]
collectResults values =
case concat [problems | Left problems <- values] of
[] -> Right (mapMaybe rightValue values)
problems -> Left problems
where
rightValue (Right value) = Just value
rightValue (Left _) = Nothing
compilerProblem :: String -> Maybe SourceSpan -> String -> Diagnostic
compilerProblem code spanValue message = Diagnostic TypeCheckerStage Error code spanValue message
entryProblem :: String -> Maybe SourceSpan -> String -> Diagnostic
entryProblem = compilerProblem