tala-compiler-0.1.0: test/SourceSetTests.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
module SourceSetTests (sourceSetTests) where
import Control.Exception (finally)
import Data.ByteString qualified as ByteString
import Data.List (sort)
import System.Directory
( canonicalizePath
, createDirectory
, createDirectoryIfMissing
, getTemporaryDirectory
, removeDirectoryRecursive
, removeFile
)
import System.FilePath (takeDirectory, (</>))
import System.IO (hClose, openTempFile)
import System.Info (os)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Compiler
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.CorePrep
import Progmasoft.Tala.Core.Wire
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.SourceSet
sourceSetTests :: [(String, IO Bool)]
sourceSetTests =
[ ("source globs distinguish segments and recursive wildcards", pure globSemantics)
, ("source loader discovers deterministic project-relative order", discoveryOrder)
, ("source loader applies project-relative exclusions", exclusionPolicy)
, ("overlapping source roots do not duplicate compilation units", overlappingRoots)
, ("uppercase source extensions are not treated as .tala", caseSensitiveExtension)
, ("malformed UTF-8 is rejected before lexing", invalidUtf8)
, ("tool source discovery does not impose compiler UTF-8", encodingNeutralDiscovery)
, ("explicit file mode uses the strict source decoder", explicitFileDecoder)
, ("explicit file mode requires the exact .tala extension", explicitFileExtension)
, ("explicit file compilation uses a safe relative Core identity", explicitFileIdentity)
, ("a configured source root cannot escape the project", escapingRoot)
, ("an empty discovered source set is diagnosed", emptySourceSet)
, ("project compiler merges files that declare one namespace", namespaceMerge)
, ("project Core keeps every source and one owner per function", sourceOwnership)
, ("source ownership survives the versioned Core wire round trip", sourceOwnershipWireRoundTrip)
, ("project source paths use portable relative spelling", portableProjectSourcePaths)
, ("empty namespace source files remain in the project catalog", emptyNamespaceSourceIsRetained)
, ("project compiler selects entry by namespace and class", entrySelection)
, ("project compiler validates every namespace", validatesEveryNamespace)
, ("project compiler accumulates independent file diagnostics", accumulatesFileDiagnostics)
, ("duplicate types across physical files are rejected", duplicateAcrossFiles)
, ("missing entry namespaces are diagnosed", missingEntryNamespace)
, ("entry names must include a namespace", unqualifiedEntry)
, ("entry Main remains public static void across merged units", mergedEntryContract)
]
globSemantics :: Bool
globSemantics =
and
[ matchesGlob "Sources/**/*.tala" "Sources/Main.tala"
, matchesGlob "Sources/**/*.tala" "Sources/Talac/Parser.tala"
, matchesGlob "Sources/Generated/**" "Sources/Generated/Nested/File.tala"
, matchesGlob "Sources/?ain.tala" "Sources/Main.tala"
, matchesGlob "Sources/*.tala" "Sources/Main.tala"
, not (matchesGlob "Sources/*.tala" "Sources/Nested/Main.tala")
, if os == "mingw32"
then matchesGlob "Sources/Main.tala" "Sources/main.tala"
else not (matchesGlob "Sources/Main.tala" "Sources/main.tala")
, not (matchesGlob "Tests/**" "Sources/Tests/Main.tala")
]
discoveryOrder :: IO Bool
discoveryOrder = withTemporaryTree $ \root -> do
writeSource root "Sources/Zeta.tala" minimalSource
writeSource root "Sources/Alpha.tala" minimalSource
writeSource root "Sources/Nested/Middle.tala" minimalSource
result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
pure $ case result of
Right sources ->
map loadedSourceRelativePath sources
== ["Sources/Alpha.tala", "Sources/Nested/Middle.tala", "Sources/Zeta.tala"]
Left _ -> False
exclusionPolicy :: IO Bool
exclusionPolicy = withTemporaryTree $ \root -> do
writeSource root "Sources/Main.tala" minimalSource
writeSource root "Sources/Generated/Bindings.tala" "this would not parse"
writeSource root "Sources/Skip.tala" "this would not parse"
writeSource root "Sources/Nested/Keep.tala" minimalSource
result <-
loadSourceSet
( SourceSetRequest
root
["Sources"]
["Sources/Generated/**", "Sources/Skip.tala"]
)
pure $ case result of
Right sources ->
map loadedSourceRelativePath sources
== ["Sources/Main.tala", "Sources/Nested/Keep.tala"]
Left _ -> False
overlappingRoots :: IO Bool
overlappingRoots = withTemporaryTree $ \root -> do
writeSource root "Sources/Main.tala" minimalSource
writeSource root "Sources/Nested/Helper.tala" minimalSource
result <- loadSourceSet (SourceSetRequest root ["Sources", "Sources/Nested"] [])
pure $ case result of
Right sources ->
map loadedSourceRelativePath sources
== ["Sources/Main.tala", "Sources/Nested/Helper.tala"]
Left _ -> False
caseSensitiveExtension :: IO Bool
caseSensitiveExtension = withTemporaryTree $ \root -> do
writeSource root "Sources/Main.TALA" minimalSource
result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
pure (hasDiagnostic "TAS0007" result)
invalidUtf8 :: IO Bool
invalidUtf8 = withTemporaryTree $ \root -> do
let path = root </> "Sources" </> "Invalid.tala"
createDirectoryIfMissing True (root </> "Sources")
ByteString.writeFile path (ByteString.pack [0x63, 0x6c, 0x61, 0x73, 0x73, 0x20, 0xc3, 0x28])
result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
pure (hasDiagnostic "TAS0020" result)
encodingNeutralDiscovery :: IO Bool
encodingNeutralDiscovery = withTemporaryTree $ \root -> do
let path = root </> "Sources" </> "Utf16.tala"
createDirectoryIfMissing True (root </> "Sources")
ByteString.writeFile path (ByteString.pack [0xff, 0xfe, 0x63, 0x00])
expected <- canonicalizePath path
result <- discoverSourceSet (SourceSetRequest root ["Sources"] [])
pure $ case result of
Right [discovered] -> discovered == expected
_ -> False
explicitFileDecoder :: IO Bool
explicitFileDecoder = withTemporaryTree $ \root -> do
let path = root </> "Türkçe.tala"
ByteString.writeFile path (ByteString.pack [0xc3, 0x28])
result <- loadSourceFile path
pure (hasDiagnostic "TAS0020" result)
explicitFileExtension :: IO Bool
explicitFileExtension = withTemporaryTree $ \root -> do
let path = root </> "Main.TALA"
writeFile path minimalSource
result <- loadSourceFile path
pure (hasDiagnostic "TAS0003" result)
explicitFileIdentity :: IO Bool
explicitFileIdentity = withTemporaryTree $ \root -> do
writeSource root "Sources/Main.tala" minimalSource
let path = root </> "Sources" </> "Main.tala"
loaded <- loadSourceFile path
pure $ case loaded of
Left _ -> False
Right source ->
loadedSourceRelativePath source == "Main.tala"
&& case compileToCorePrep
(CompilerInput (loadedSourceRelativePath source) (loadedSourceText source)) of
Left _ -> False
Right artifacts ->
let owners = map snd (coreModuleFunctionSources (artifactOptimizedCore artifacts))
in not (null owners) && all (== "Main.tala") owners
escapingRoot :: IO Bool
escapingRoot = withTemporaryTree $ \root -> do
let project = root </> "Project"
outside = root </> "Outside"
createDirectory project
createDirectory outside
writeSource outside "Escape.tala" minimalSource
result <- loadSourceSet (SourceSetRequest project ["../Outside"] [])
pure (hasDiagnostic "TAS0009" result)
emptySourceSet :: IO Bool
emptySourceSet = withTemporaryTree $ \root -> do
createDirectoryIfMissing True (root </> "Sources")
writeFile (root </> "Sources" </> "README.txt") "not source"
result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
pure (hasDiagnostic "TAS0007" result)
namespaceMerge :: IO Bool
namespaceMerge = pure $ case compileProjectToCorePrep applicationEntry inputs of
Right project ->
length (projectNamespaces project) == 1
&& artifactSourceFiles (projectEntryNamespace project) == ["helper.tala", "main.tala"]
&& length (coreModuleFunctions (projectEntryCore project)) == 2
Left _ -> False
where
inputs =
[ CompilerInput "main.tala" applicationMain
, CompilerInput "helper.tala" applicationHelper
]
-- Physical source ownership is carried beside semantic symbols. It must stay
-- deterministic as files are merged, lowered, and written to the Core wire.
sourceOwnership :: IO Bool
sourceOwnership = pure $ case compileProjectToCorePrep applicationEntry inputs of
Right project ->
let core = projectEntryCore project
prepared = artifactCorePrep (artifactFrontend (projectEntryNamespace project))
expectedSources = ["helper.tala", "main.tala"]
owners = map snd (coreModuleFunctionSources core)
in sort (coreModuleSourceFiles core) == expectedSources
&& sort (corePrepModuleSourceFiles prepared) == expectedSources
&& sort owners == expectedSources
&& sort (map corePrepFunctionSourceFile (corePrepModuleFunctions prepared)) == expectedSources
&& all (`elem` coreModuleSourceFiles core) owners
Left _ -> False
where
inputs =
[ CompilerInput "main.tala" applicationMain
, CompilerInput "helper.tala" applicationHelper
]
sourceOwnershipWireRoundTrip :: IO Bool
sourceOwnershipWireRoundTrip = pure $ case compileProjectToCorePrep applicationEntry inputs of
Right project ->
let core = projectEntryCore project
in case encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits of
Right decoded ->
coreModuleSourceFiles decoded == coreModuleSourceFiles core
&& coreModuleFunctionSources decoded == coreModuleFunctionSources core
&& decoded == core
Left _ -> False
Left _ -> False
where
inputs =
[ CompilerInput "main.tala" applicationMain
, CompilerInput "helper.tala" applicationHelper
]
portableProjectSourcePaths :: IO Bool
portableProjectSourcePaths = pure $ case compileProjectToCorePrep applicationEntry inputs of
Right project ->
let core = projectEntryCore project
paths = coreModuleSourceFiles core ++ map snd (coreModuleFunctionSources core)
in paths == ["Sources/helper.tala", "Sources/main.tala", "Sources/helper.tala", "Sources/main.tala"]
&& all (not . any (== '\\')) paths
Left _ -> False
where
inputs =
[ CompilerInput "Sources\\main.tala" applicationMain
, CompilerInput "Sources\\helper.tala" applicationHelper
]
emptyNamespaceSourceIsRetained :: IO Bool
emptyNamespaceSourceIsRetained = pure $ case compileProjectToCorePrep applicationEntry inputs of
Right project ->
let core = projectEntryCore project
prepared = artifactCorePrep (artifactFrontend (projectEntryNamespace project))
in sort (coreModuleSourceFiles core) == ["empty.tala", "helper.tala", "main.tala"]
&& sort (corePrepModuleSourceFiles prepared) == ["empty.tala", "helper.tala", "main.tala"]
&& length (coreModuleFunctions core) == 2
Left _ -> False
where
inputs =
[ CompilerInput "main.tala" applicationMain
, CompilerInput "helper.tala" applicationHelper
, CompilerInput "empty.tala" "namespace Application;"
]
entrySelection :: IO Bool
entrySelection = pure $ case compileProjectToCorePrep applicationEntry inputs of
Right project ->
length (projectNamespaces project) == 2
&& artifactNamespace (projectEntryNamespace project) == Just applicationNamespace
&& coreModuleName (projectEntryCore project) == applicationNamespace
Left _ -> False
where
inputs =
[ CompilerInput "library.tala" "namespace Library; class Utility { public static int Value() { return 7; } }"
, CompilerInput "main.tala" applicationMain
]
validatesEveryNamespace :: IO Bool
validatesEveryNamespace =
pure $
hasDiagnostic
"TAN0001"
( compileProjectToCorePrep
applicationEntry
[ CompilerInput "main.tala" applicationMain
, CompilerInput "broken.tala" "namespace Broken; class Utility { int Value() { return missing; } }"
]
)
accumulatesFileDiagnostics :: IO Bool
accumulatesFileDiagnostics = pure $ case compileProjectToCorePrep applicationEntry inputs of
Left diagnostics ->
length diagnostics == 2
&& map (fmap sourceFile . diagnosticSpan) diagnostics
== [Just "first.tala", Just "second.tala"]
Right _ -> False
where
inputs =
[ CompilerInput "first.tala" "namespace First; class One { void Run() { @; } }"
, CompilerInput "second.tala" "namespace Second; class Two { void Run() { #; } }"
]
duplicateAcrossFiles :: IO Bool
duplicateAcrossFiles =
pure $
hasDiagnostic
"TAR0001"
( compileProjectToCorePrep
applicationEntry
[ CompilerInput "first.tala" applicationMain
, CompilerInput "second.tala" "namespace Application; class Program { public static void Main() { return; } }"
]
)
missingEntryNamespace :: IO Bool
missingEntryNamespace =
pure $
hasDiagnostic
"TAE0011"
( compileProjectToCorePrep
(QualifiedName [Identifier "Missing", Identifier "Program"])
[CompilerInput "main.tala" applicationMain]
)
unqualifiedEntry :: IO Bool
unqualifiedEntry =
pure $
hasDiagnostic
"TAE0001"
( compileProjectToCorePrep
(QualifiedName [Identifier "Program"])
[CompilerInput "main.tala" applicationMain]
)
mergedEntryContract :: IO Bool
mergedEntryContract =
pure $
hasDiagnostic
"TAE0008"
( compileProjectToCorePrep
applicationEntry
[ CompilerInput "helper.tala" applicationHelper
, CompilerInput
"main.tala"
"namespace Application; class Program { public static int Main() { return 0; } }"
]
)
applicationNamespace :: QualifiedName
applicationNamespace = QualifiedName [Identifier "Application"]
applicationEntry :: QualifiedName
applicationEntry = QualifiedName [Identifier "Application", Identifier "Program"]
applicationMain :: String
applicationMain =
unlines
[ "namespace Application;"
, "class Program {"
, " public static void Main() {"
, " return;"
, " }"
, "}"
]
applicationHelper :: String
applicationHelper =
unlines
[ "namespace Application;"
, "class Helper {"
, " public static int Value() {"
, " return 42;"
, " }"
, "}"
]
minimalSource :: String
minimalSource = "namespace Example; class Value { public static int Get() { return 1; } }"
hasDiagnostic :: String -> Either [Diagnostic] value -> Bool
hasDiagnostic code result = case result of
Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
Right _ -> False
withTemporaryTree :: (FilePath -> IO Bool) -> IO Bool
withTemporaryTree action = do
temporary <- getTemporaryDirectory
(path, handle) <- openTempFile temporary "tala-source-set-"
hClose handle
removeFile path
createDirectory path
action path `finally` removeDirectoryRecursive path
writeSource :: FilePath -> FilePath -> String -> IO ()
writeSource root relative content = do
let path = root </> relative
createDirectoryIfMissing True (takeDirectory path)
writeFile path content