summaryrefslogtreecommitdiff
path: root/source/Felix/Test/Unit/Source.hs
diff options
context:
space:
mode:
authoradelon <22380201+adelon@users.noreply.github.com>2026-08-06 17:54:00 +0200
committeradelon <22380201+adelon@users.noreply.github.com>2026-08-06 17:54:00 +0200
commit82328890108bae64b372b8d58620ebc62699de76 (patch)
tree575404c6b425c19259c0ded296f1c8ffb7ff0e2b /source/Felix/Test/Unit/Source.hs
parent1a25421c2a168d420581358c8733fcd8f36f379b (diff)
Migrate to `Felix` namespaceHEADhotg
Diffstat (limited to 'source/Felix/Test/Unit/Source.hs')
-rw-r--r--source/Felix/Test/Unit/Source.hs2581
1 files changed, 2581 insertions, 0 deletions
diff --git a/source/Felix/Test/Unit/Source.hs b/source/Felix/Test/Unit/Source.hs
new file mode 100644
index 0000000..6216381
--- /dev/null
+++ b/source/Felix/Test/Unit/Source.hs
@@ -0,0 +1,2581 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PatternSynonyms #-}
+
+module Felix.Test.Unit.Source (unitTests) where
+
+import Base
+import Felix.Checking.Foundation qualified as Foundation
+import Felix.Checking.Identity qualified as Identity
+import Felix.Checking.Semantic qualified as Semantic
+import Felix.Cache.Codec qualified as Cache
+import Felix.Module qualified as Module
+import Felix.Parse qualified as Parse
+import Felix.Parsed.Identity qualified as ParsedIdentity
+import Felix.Parsed.Payload qualified as Parsed
+import Felix.Prelude qualified as Prelude
+import Felix.Source
+import Felix.Source.Content qualified as Content
+import Felix.Source.Graph
+import Felix.Store qualified as Store
+import Felix.Report.Location
+ ( FileId(..)
+ , FileIdAllocator(..)
+ , Location(..)
+ , LocationRegistrationError(..)
+ , allocateFileId
+ , locColumn
+ , locFile
+ , locFileId
+ , locLine
+ , lookupFileIdentityPath
+ )
+import Felix.Syntax.Abstract qualified as Raw
+import Felix.Syntax.Adapt qualified as Adapt
+import Felix.Syntax.Interface qualified as Interface
+import Felix.Syntax.Token (runLexer)
+
+import Control.Exception (bracket, evaluate)
+import Data.ByteString qualified as ByteString
+import Data.IORef
+import Data.List qualified as List
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Text qualified as Text
+import Data.Word (Word8, Word16)
+import Database.SQLite.Simple qualified as SQLite
+import System.Directory qualified as Directory
+import System.FilePath.Posix qualified as Posix
+import System.Posix.Files qualified as PosixFiles
+import Test.Tasty
+import Test.Tasty.HUnit
+
+
+unitTests :: TestTree
+unitTests = testGroup "Source resolution"
+ [ testCase "validates mount-root-relative POSIX paths" validatesRelativePaths
+ , testCase "rejects duplicate source mount ids" rejectsDuplicateMountIds
+ , testCase "rejects duplicate canonical mount roots" rejectsDuplicateMountRoots
+ , testCase "permits missing and rejects non-directory mounts"
+ validatesMountRootTypes
+ , testCase "rejects relative exact roots" rejectsRelativeExactRoots
+ , testCase "retains exact root spelling as diagnostic trivia" retainsRootSpelling
+ , testCase "searched and exact roots share canonical identity" rootFormsShareIdentity
+ , testCase "attributes nested sources to the most specific mount" attributesNestedSources
+ , testCase "configured order selects searched candidates" candidateOrderSelectsWinner
+ , testCase "rejects a higher-priority special source"
+ rejectsHigherPrioritySpecialSource
+ , testCase "rejects exact roots outside configured mounts" rejectsOutsideExactRoot
+ , testCase "loads source text as strict UTF-8" loadsStrictUtf8
+ , testCase "reports malformed UTF-8 sequence starts"
+ reportsInvalidUtf8Offsets
+ , testCase "reserves the all-ones file identifier"
+ preservesReservedFileId
+ , testCase "builds an imported-before-importer source graph" buildsSourceGraph
+ , testCase "rejects the packaged prelude as ordinary source"
+ rejectsPackagedPreludeAsOrdinarySource
+ , testCase "orders sibling imports by textual occurrence"
+ ordersSiblingImports
+ , testCase "orders shared dependencies before their importers"
+ ordersSharedDependencies
+ , testCase "retains repeated import-edge occurrences" retainsRepeatedImports
+ , testCase "deduplicates canonical source nodes" deduplicatesCanonicalNodes
+ , testCase "reports missing imports at their source location" reportsMissingImports
+ , testCase "rejects unsafe imports at their source location" rejectsUnsafeImports
+ , testCase "reports the located import cycle chain" reportsImportCycles
+ , testCase "rejects malformed imported source before discovery" rejectsMalformedImportedSource
+ , testCase "builds empty modules through the ordinary pipeline"
+ buildsEmptyModules
+ , testCase "identifies owner-independent parsed modules"
+ identifiesOwnerIndependentParsedModules
+ , testCase "keys effective direct syntax inputs"
+ keysEffectiveDirectSyntaxInputs
+ , testCase "reuses exact parsed syntax on a warm pass"
+ reusesExactParsedSyntax
+ , testCase "invalidates exact parsed inputs transitively"
+ invalidatesExactParsedInputs
+ , testCase "rebinds relocated parsed artifacts"
+ rebindsRelocatedParsedArtifacts
+ , testCase "rejects a corrupted cached declaration anchor"
+ rejectsCorruptedCachedDeclarationAnchor
+ , testCase "parses source-local blocks in graph order" parsesSourceGraph
+ , testCase "does not leak syntax between sibling imports"
+ rejectsSiblingSyntaxLeakage
+ , testCase "parses source fixity levels and grouping"
+ parsesSourceFixities
+ , testCase "parses cdot and symdiff fixities"
+ parsesLibraryFixities
+ , testCase "validates source pragma associations"
+ validatesSourcePragmaAssociations
+ , testCase "rejects fixed-base category mismatches"
+ rejectsFixedBaseCategoryMismatch
+ , testCase "retains multi-item syntax occurrence order"
+ retainsMultiItemSyntaxOccurrences
+ , testCase "propagates and coalesces imported syntax"
+ propagatesImportedSyntax
+ , testCase "rejects unequal imported syntax"
+ rejectsUnequalImportedSyntax
+ , testCase "qualifies same-display cross-mount collisions"
+ distinguishesPhysicalSourceLocations
+ , testCase "retains each workspace location display path"
+ retainsWorkspaceLocationDisplayPath
+ , testCase "reports imported scanner errors before importer tokenizer errors"
+ reportsImportedScannerErrorFirst
+ , testCase "returns malformed lexical declarations as typed errors"
+ reportsMalformedLexicalDeclaration
+ , testCase "validates inductive function patterns during scanning"
+ rejectsMalformedInductivePattern
+ , testCase "scans and parses adjective signatures"
+ acceptsAdjectiveSignature
+ , testCase "rejects malformed math-led signature heads"
+ rejectsMalformedSignatureHead
+ , testCase "locates conflicting declarations within one environment"
+ reportsSameSourceLexiconCollision
+ , testCase "accepts the first source declaration of a built-in pattern"
+ acceptsBuiltinSourceDeclaration
+ , testCase "keeps the built-in marker for a prefix predicate declaration"
+ acceptsBuiltinPrefixPredicateDeclaration
+ , testCase "does not rescan repeated canonical imports"
+ avoidsAliasImportLexiconCollision
+ , testCase "parses loaded sources without rereading files" parsesWithoutRereading
+ , testCase "returns source-local failures after prior chunk callbacks"
+ returnsSourceParseFailures
+ , testCase "rejects guarded symbolic declarations before publication"
+ rejectsGuardedSymbolicDeclarations
+ ]
+
+validatesRelativePaths :: Assertion
+validatesRelativePaths = do
+ assertRight (safeRelativePath "theory/set.tex")
+ assertRight (safeRelativePath "theory\\set.tex")
+ assertLeft EmptyRelativePath (safeRelativePath "")
+ assertLeft AbsoluteRelativePath (safeRelativePath "/theory.tex")
+ assertLeft CurrentDirectoryComponent (safeRelativePath "./theory.tex")
+ assertLeft ParentDirectoryComponent (safeRelativePath "a/../theory.tex")
+ assertLeft EmptyPathComponent (safeRelativePath "a//theory.tex")
+ assertLeft EmptyPathComponent (safeRelativePath "a/")
+ assertLeft NullPathCharacter (safeRelativePath "a\0b")
+
+rejectsDuplicateMountIds :: Assertion
+rejectsDuplicateMountIds =
+ withTemporaryDirectory "felix-source-duplicate-id" \temp -> do
+ result <- prepareSourceMounts
+ [ (sourceMountId "same", temp Posix.</> "one")
+ , (sourceMountId "same", temp Posix.</> "two")
+ ]
+ assertEqual
+ "duplicate id"
+ (Left (DuplicateSourceMountId (sourceMountId "same")))
+ result
+
+rejectsDuplicateMountRoots :: Assertion
+rejectsDuplicateMountRoots =
+ withTemporaryDirectory "felix-source-duplicate-root" \temp -> do
+ result <- prepareSourceMounts
+ [ (sourceMountId "one", temp)
+ , (sourceMountId "two", temp Posix.</> ".")
+ ]
+ canonical <- Directory.canonicalizePath temp
+ case result of
+ Left (DuplicateCanonicalMountRoot root firstId secondId) -> do
+ assertEqual "canonical root" canonical (canonicalPathFilePath root)
+ assertEqual "first mount id" (sourceMountId "one") firstId
+ assertEqual "second mount id" (sourceMountId "two") secondId
+ Left err ->
+ assertFailure ("expected DuplicateCanonicalMountRoot, got " <> show err)
+ Right mounts ->
+ assertFailure ("expected duplicate-root rejection, got " <> show mounts)
+
+validatesMountRootTypes :: Assertion
+validatesMountRootTypes =
+ withTemporaryDirectory "felix-source-mount-type" \temp -> do
+ let ident = sourceMountId "project"
+ missing = temp Posix.</> "missing"
+ regularFile = temp Posix.</> "file"
+ assertRight =<< prepareSourceMounts [(ident, missing)]
+
+ writeFile regularFile ""
+ result <- prepareSourceMounts [(ident, regularFile)]
+ case result of
+ Left SourceMountNotDirectory{} ->
+ pure ()
+ Left err ->
+ assertFailure
+ ("expected SourceMountNotDirectory, got " <> show err)
+ Right mounts ->
+ assertFailure
+ ("expected non-directory rejection, got " <> show mounts)
+
+rejectsRelativeExactRoots :: Assertion
+rejectsRelativeExactRoots =
+ assertEqual
+ "relative exact roots are rejected"
+ (Left (ExistingRootNotAbsolute "entry.tex"))
+ =<< existingRoot "entry.tex"
+
+retainsRootSpelling :: Assertion
+retainsRootSpelling =
+ withTemporaryDirectory "felix-source-root-spelling" \temp -> do
+ let source = temp Posix.</> "entry.tex"
+ alias = temp Posix.</> "entry-alias.tex"
+ writeFile source ""
+ Directory.createFileLink source alias
+ direct <- expectRight =<< existingRoot source
+ throughAlias <- expectRight =<< existingRoot alias
+ assertEqual "canonical request identity" direct throughAlias
+ assertEqual "diagnostic spelling" alias
+ (rootRequestSpelling throughAlias)
+
+rootFormsShareIdentity :: Assertion
+rootFormsShareIdentity =
+ withTemporaryDirectory "felix-source-root-identity" \temp -> do
+ let source = temp Posix.</> "entry.tex"
+ writeFile source "source"
+ mounts <- oneMount "project" temp
+ searched <- expectRight (searchedRoot "entry.tex")
+ exact <- expectRight =<< existingRoot source
+ searchedLoaded <- expectRight =<< resolveAndLoadRoot mounts searched
+ exactLoaded <- expectRight =<< resolveAndLoadRoot mounts exact
+ assertEqual "loaded source" searchedLoaded exactLoaded
+ assertEqual "source mount"
+ (resolvedSourceMount (loadedSource searchedLoaded))
+ (resolvedSourceMount (loadedSource exactLoaded))
+ assertEqual "mount-relative source path"
+ (resolvedSourceRelativePath (loadedSource searchedLoaded))
+ (resolvedSourceRelativePath (loadedSource exactLoaded))
+
+rejectsPackagedPreludeAsOrdinarySource :: Assertion
+rejectsPackagedPreludeAsOrdinarySource = do
+ packaged <- expectRight =<< Prelude.loadReservedPreludeSourceInput
+ canonical <- expectJust "packaged canonical path"
+ (Prelude.reservedPreludeSourceCanonicalPath packaged)
+ let path = canonicalPathFilePath canonical
+ mounts <- oneMount "packaged" (Posix.takeDirectory path)
+ request <- expectRight (searchedRoot (Posix.takeFileName path))
+ let validate = Prelude.rejectOrdinaryPreludeSourceGraph packaged
+ syntaxInputs = const []
+ Parse.parseSourceWorkspaceWithSyntaxInputsAndGraphValidation
+ mounts request syntaxInputs validate >>= \case
+ Left
+ (Parse.SourceWorkspaceError
+ (PackagedPreludeSelectedAsOrdinarySource source)) ->
+ assertEqual "authority-free rejected path"
+ canonical
+ (resolvedSourceCanonicalPath source)
+ other ->
+ assertFailure
+ ("unexpected authority-free result: " <> show other)
+
+ withTemporaryDirectory "felix-reserved-parse-store" \temp -> do
+ foundation <- expectRight Foundation.checkedFoundation
+ store <- openTestStore
+ (temp Posix.</> "store.sqlite")
+ (Identity.theoryId foundation)
+ Parse.parseSourceWorkspaceWithStoreAndSyntaxInputsAndGraphValidation
+ store mounts request syntaxInputs validate >>= \case
+ Left
+ (Parse.ParseExecutionWorkspaceError
+ (Parse.SourceWorkspaceError
+ (PackagedPreludeSelectedAsOrdinarySource source))) ->
+ assertEqual "typed rejected path"
+ canonical
+ (resolvedSourceCanonicalPath source)
+ other ->
+ assertFailure
+ ("unexpected typed result: " <> show other)
+ Store.closeStore store
+
+attributesNestedSources :: Assertion
+attributesNestedSources =
+ withTemporaryDirectory "felix-source-nested-mount" \temp -> do
+ let nested = temp Posix.</> "library"
+ source = nested Posix.</> "entry.tex"
+ Directory.createDirectory nested
+ writeFile source "source"
+ exact <- expectRight =<< existingRoot source
+ outerFirst <- expectRight =<< prepareSourceMounts
+ [ (sourceMountId "project", temp)
+ , (sourceMountId "library", nested)
+ ]
+ innerFirst <- expectRight =<< prepareSourceMounts
+ [ (sourceMountId "library", nested)
+ , (sourceMountId "project", temp)
+ ]
+ searched <- expectRight (searchedRoot "library/entry.tex")
+ outerFirstSource <- loadedSource <$> (expectRight =<< resolveAndLoadRoot outerFirst exact)
+ innerFirstSource <- loadedSource <$> (expectRight =<< resolveAndLoadRoot innerFirst exact)
+ searchedSource <- loadedSource <$> (expectRight =<< resolveAndLoadRoot outerFirst searched)
+ assertEqual "order-independent attribution" outerFirstSource innerFirstSource
+ assertEqual "root-form-independent attribution" outerFirstSource searchedSource
+ assertEqual "most specific mount" (sourceMountId "library") (resolvedSourceMount outerFirstSource)
+ assertEqual "mount-relative identity" "entry.tex"
+ (safeRelativePathFilePath (resolvedSourceRelativePath outerFirstSource))
+
+candidateOrderSelectsWinner :: Assertion
+candidateOrderSelectsWinner =
+ withTemporaryDirectory "felix-source-precedence" \temp -> do
+ let firstRoot = temp Posix.</> "first"
+ secondRoot = temp Posix.</> "second"
+ firstSource = firstRoot Posix.</> "entry.tex"
+ secondSource = secondRoot Posix.</> "entry.tex"
+ Directory.createDirectory firstRoot
+ Directory.createDirectory secondRoot
+ writeFile firstSource "first"
+ writeFile secondSource "second"
+ request <- expectRight (searchedRoot "entry.tex")
+ firstMounts <- expectRight =<< prepareSourceMounts
+ [ (sourceMountId "first", firstRoot)
+ , (sourceMountId "second", secondRoot)
+ ]
+ secondMounts <- expectRight =<< prepareSourceMounts
+ [ (sourceMountId "second", secondRoot)
+ , (sourceMountId "first", firstRoot)
+ ]
+ firstWinner <- expectRight =<< resolveAndLoadRoot firstMounts request
+ secondWinner <- expectRight =<< resolveAndLoadRoot secondMounts request
+ assertEqual "first configured source" "first" (loadedText firstWinner)
+ assertEqual "reversed configured source" "second" (loadedText secondWinner)
+
+rejectsHigherPrioritySpecialSource :: Assertion
+rejectsHigherPrioritySpecialSource =
+ withTemporaryDirectory "felix-source-special-precedence" \temp -> do
+ let higherRoot = temp Posix.</> "higher"
+ lowerRoot = temp Posix.</> "lower"
+ higherSource = higherRoot Posix.</> "entry.tex"
+ lowerSource = lowerRoot Posix.</> "entry.tex"
+ Directory.createDirectory higherRoot
+ Directory.createDirectory lowerRoot
+ PosixFiles.createNamedPipe higherSource PosixFiles.ownerModes
+ writeFile lowerSource "ordinary source"
+ mounts <- expectRight =<< prepareSourceMounts
+ [ (sourceMountId "higher", higherRoot)
+ , (sourceMountId "lower", lowerRoot)
+ ]
+ request <- expectRight (searchedRoot "entry.tex")
+ result <- resolveRoot mounts request
+ case result of
+ Left
+ (SelectedSourceNotRegular
+ (SearchedRootLookup relative)
+ selectedPath
+ canonical) -> do
+ assertEqual "searched path"
+ "entry.tex"
+ (safeRelativePathFilePath relative)
+ assertEqual "selected higher candidate"
+ higherSource
+ selectedPath
+ canonicalHigher <-
+ Directory.canonicalizePath higherSource
+ assertEqual "selected canonical target"
+ canonicalHigher
+ (canonicalPathFilePath canonical)
+ Left err ->
+ assertFailure
+ ("expected SelectedSourceNotRegular, got " <> show err)
+ Right source ->
+ assertFailure
+ ("expected special-source rejection, got " <> show source)
+
+rejectsOutsideExactRoot :: Assertion
+rejectsOutsideExactRoot =
+ withTemporaryDirectory "felix-source-outside-root" \temp -> do
+ let mountRoot = temp Posix.</> "mount"
+ outsideRoot = temp Posix.</> "outside"
+ source = outsideRoot Posix.</> "entry.tex"
+ Directory.createDirectory mountRoot
+ Directory.createDirectory outsideRoot
+ writeFile source "source"
+ mounts <- oneMount "project" mountRoot
+ exact <- expectRight =<< existingRoot source
+ result <- resolveAndLoadRoot mounts exact
+ case result of
+ Left (RootOutsideConfiguredMount spelling _canonical) ->
+ assertEqual "exact-root diagnostic spelling" source spelling
+ Left err ->
+ assertFailure ("expected RootOutsideConfiguredMount, got " <> show err)
+ Right loaded ->
+ assertFailure ("expected outside-root rejection, got " <> show loaded)
+
+loadsStrictUtf8 :: Assertion
+loadsStrictUtf8 =
+ withTemporaryDirectory "felix-source-utf8" \temp -> do
+ let source = temp Posix.</> "unicode.tex"
+ bytes =
+ ByteString.pack
+ [ 0xCE, 0xB1, 0x20, 0xE2
+ , 0x88, 0x88, 0x20, 0x41
+ ]
+ ByteString.writeFile source bytes
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "unicode.tex")
+ loaded <- expectRight =<< resolveAndLoadRoot mounts request
+ assertEqual "exact bytes" bytes (loadedBytes loaded)
+ assertEqual "decoded text" ("α ∈ A" :: Text) (loadedText loaded)
+ assertEqual
+ "byte count"
+ (fromIntegral (ByteString.length bytes))
+ (loadedByteCount loaded)
+ let identifier =
+ Content.sourceContentId loaded
+ assertEqual
+ "content identity cache round trip"
+ (Right identifier)
+ (Cache.decodeCache
+ Content.getSourceContentIdCache
+ (Cache.encodeCache
+ (Content.putSourceContentIdCache
+ identifier)))
+ ByteString.writeFile source (bytes <> "\n")
+ changed <- expectRight
+ =<< loadResolvedSource (loadedSource loaded)
+ assertBool
+ "exact byte edits change source identity"
+ (identifier /= Content.sourceContentId changed)
+
+reportsInvalidUtf8Offsets :: Assertion
+reportsInvalidUtf8Offsets =
+ withTemporaryDirectory "felix-source-invalid-utf8" \temp -> do
+ let source = temp Posix.</> "invalid.tex"
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "invalid.tex")
+ let assertOffset label bytes expected = do
+ ByteString.writeFile source (ByteString.pack bytes)
+ result <- resolveAndLoadRoot mounts request
+ case result of
+ Left (SourceDecodeError _source offset) ->
+ assertEqual label expected offset
+ Left err ->
+ assertFailure
+ ("expected SourceDecodeError, got " <> show err)
+ Right loaded ->
+ assertFailure
+ ("expected malformed UTF-8 rejection, got "
+ <> show loaded)
+ assertOffset "malformed sequence start" [0x61, 0xC3, 0x28] 1
+ assertOffset "incomplete sequence start" [0x61, 0xC3] 1
+
+preservesReservedFileId :: Assertion
+preservesReservedFileId =
+ case allocateFileId boundaryAllocator of
+ Left err ->
+ assertFailure
+ ("could not allocate last available file id: " <> show err)
+ Right (fileId, exhaustedAllocator) -> do
+ assertEqual "last available file id"
+ (maxBound - 1)
+ (unFileId fileId)
+ assertBool "allocator returned reserved file id"
+ (unFileId fileId /= maxBound)
+ assertEqual "allocator reports exhaustion"
+ (Left FileIdSpaceExhausted)
+ (allocateFileId exhaustedAllocator)
+ where
+ boundaryAllocator =
+ FileIdAllocator
+ (fromIntegral (maxBound :: Word16) - 1)
+
+buildsSourceGraph :: Assertion
+buildsSourceGraph =
+ withTemporaryDirectory "felix-source-graph" \temp -> do
+ writeTheory (temp Posix.</> "shared.tex") [] "shared"
+ writeTheory (temp Posix.</> "entry.tex") ["shared.tex"] "entry"
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ graph <- expectRight =<< buildResolvedSourceGraph mounts request
+ assertEqual "two source nodes" 2 (length (sourceGraphNodes graph))
+ case sourceGraphImportEdges graph of
+ [edge] -> do
+ assertEqual "root imports" (sourceGraphRoot graph) (sourceImportingNode edge)
+ assertEqual
+ "imported-before-importer order"
+ [sourceImportedNode edge, sourceGraphRoot graph]
+ ( sourceNodeCanonicalPathForTest
+ <$> toList
+ (sourceGraphImportedBeforeImporter graph)
+ )
+ assertEqual "import location line" 1
+ (locLine (importLocation (sourceImportReference edge)))
+ assertEqual "selected location path" "entry.tex"
+ (locFile (importLocation (sourceImportReference edge)))
+ edges ->
+ assertFailure ("expected one import edge, got " <> show edges)
+
+ordersSiblingImports :: Assertion
+ordersSiblingImports =
+ withTemporaryDirectory "felix-source-sibling-order" \temp -> do
+ writeTheory (temp Posix.</> "a.tex") [] "a"
+ writeTheory (temp Posix.</> "b.tex") [] "b"
+ writeTheory
+ (temp Posix.</> "entry.tex")
+ ["a.tex", "b.tex"]
+ "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ order <- sourceGraphOrderPaths graph
+ assertEqual "DFS completion order"
+ ["a.tex", "b.tex", "entry.tex"]
+ order
+
+ordersSharedDependencies :: Assertion
+ordersSharedDependencies =
+ withTemporaryDirectory "felix-source-shared-order" \temp -> do
+ writeTheory (temp Posix.</> "shared.tex") [] "shared"
+ writeTheory (temp Posix.</> "a.tex") ["shared.tex"] "a"
+ writeTheory (temp Posix.</> "b.tex") ["shared.tex"] "b"
+ writeTheory
+ (temp Posix.</> "entry.tex")
+ ["a.tex", "b.tex"]
+ "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ order <- sourceGraphOrderPaths graph
+ assertEqual "shared dependency occurs once before both importers"
+ ["shared.tex", "a.tex", "b.tex", "entry.tex"]
+ order
+
+retainsRepeatedImports :: Assertion
+retainsRepeatedImports =
+ withTemporaryDirectory "felix-source-repeated-import" \temp -> do
+ writeTheory (temp Posix.</> "shared.tex") [] "shared"
+ writeTheory
+ (temp Posix.</> "entry.tex")
+ ["shared.tex", "shared.tex"]
+ "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ assertEqual "canonical node count" 2 (length (sourceGraphNodes graph))
+ assertEqual "repeated edge count" 2 (length (sourceGraphImportEdges graph))
+
+deduplicatesCanonicalNodes :: Assertion
+deduplicatesCanonicalNodes =
+ withTemporaryDirectory "felix-source-canonical-dedup" \temp -> do
+ let shared = temp Posix.</> "shared.tex"
+ alias = temp Posix.</> "alias.tex"
+ writeTheory shared [] "shared"
+ Directory.createFileLink shared alias
+ writeTheory
+ (temp Posix.</> "entry.tex")
+ ["shared.tex", "alias.tex"]
+ "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ assertEqual "one node for symlink aliases" 2 (length (sourceGraphNodes graph))
+ case sourceGraphImportEdges graph of
+ [firstEdge, secondEdge] ->
+ assertEqual
+ "both occurrences reach one node"
+ (sourceImportedNode firstEdge)
+ (sourceImportedNode secondEdge)
+ edges ->
+ assertFailure ("expected two import edges, got " <> show edges)
+
+reportsMissingImports :: Assertion
+reportsMissingImports =
+ withTemporaryDirectory "felix-source-missing-import" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "% heading"
+ , "\\import{missing.tex}"
+ , theoryBlock "entry"
+ ])
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ result <- buildResolvedSourceGraph mounts request
+ case result of
+ Left (SourceNotFound (ImportedSourceLookup _ reference) _candidates) -> do
+ assertEqual "missing import line" 2 (locLine (importLocation reference))
+ assertEqual "missing import source" "entry.tex"
+ (locFile (importLocation reference))
+ Left err ->
+ assertFailure ("expected located SourceNotFound, got " <> show err)
+ Right graph ->
+ assertFailure ("expected missing-import rejection, got " <> show graph)
+
+rejectsUnsafeImports :: Assertion
+rejectsUnsafeImports =
+ withTemporaryDirectory "felix-source-unsafe-import" \temp -> do
+ writeTheory (temp Posix.</> "entry.tex") ["./shared.tex"] "entry"
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ result <- buildResolvedSourceGraph mounts request
+ case result of
+ Left (InvalidImportPath _source location raw CurrentDirectoryComponent) -> do
+ assertEqual "raw import" "./shared.tex" raw
+ assertEqual "unsafe import line" 1 (locLine location)
+ assertEqual "unsafe import source" "entry.tex" (locFile location)
+ Left err ->
+ assertFailure ("expected InvalidImportPath, got " <> show err)
+ Right graph ->
+ assertFailure ("expected unsafe-import rejection, got " <> show graph)
+
+reportsImportCycles :: Assertion
+reportsImportCycles =
+ withTemporaryDirectory "felix-source-cycle" \temp -> do
+ writeTheory (temp Posix.</> "a.tex") ["b.tex"] "a"
+ writeTheory (temp Posix.</> "b.tex") ["a.tex"] "b"
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "a.tex")
+ result <- buildResolvedSourceGraph mounts request
+ case result of
+ Left (SourceImportCycle steps) -> do
+ assertEqual "cycle length" 2 (length steps)
+ assertEqual
+ "cycle importer sequence"
+ ["a.tex", "b.tex"]
+ [ safeRelativePathFilePath
+ (resolvedSourceRelativePath (cycleImporter step))
+ | step <- toList steps
+ ]
+ assertEqual
+ "cycle import locations"
+ ["a.tex", "b.tex"]
+ [ locFile (importLocation (cycleImport step))
+ | step <- toList steps
+ ]
+ Left err ->
+ assertFailure ("expected SourceImportCycle, got " <> show err)
+ Right graph ->
+ assertFailure ("expected cycle rejection, got " <> show graph)
+
+rejectsMalformedImportedSource :: Assertion
+rejectsMalformedImportedSource =
+ withTemporaryDirectory "felix-source-import-utf8" \temp -> do
+ writeTheory (temp Posix.</> "entry.tex") ["bad.tex"] "entry"
+ ByteString.writeFile
+ (temp Posix.</> "bad.tex")
+ (ByteString.pack [0x61, 0xFF])
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ result <- buildResolvedSourceGraph mounts request
+ case result of
+ Left (SourceDecodeError source offset) -> do
+ assertEqual "bad source" "bad.tex"
+ (safeRelativePathFilePath (resolvedSourceRelativePath source))
+ assertEqual "bad byte offset" 1 offset
+ Left err ->
+ assertFailure ("expected SourceDecodeError, got " <> show err)
+ Right graph ->
+ assertFailure ("expected malformed-source rejection, got " <> show graph)
+
+buildsEmptyModules :: Assertion
+buildsEmptyModules =
+ withTemporaryDirectory "felix-source-empty" \temp ->
+ forM_
+ [ ("empty.tex", "")
+ , ("comments.tex", "% heading\n% body")
+ ]
+ \(relative, contents) -> do
+ writeFile (temp Posix.</> relative) contents
+ graph <- buildSearchedGraph temp relative
+ assertEqual
+ "one ordinary graph node"
+ 1
+ (length (sourceGraphNodes graph))
+ emittedRef <- newIORef (0 :: Int)
+ workspace <-
+ expectRight
+ =<< Parse.parseResolvedSourceGraphWith
+ graph
+ (\_source _block ->
+ modifyIORef' emittedRef (+ 1))
+ assertEqual
+ "no block callbacks"
+ 0
+ =<< readIORef emittedRef
+ assertEqual
+ "empty parsed projection"
+ []
+ (Parse.importedBeforeImporterBlocks workspace)
+ assertBool
+ "empty syntax declarations"
+ (null
+ (Interface.canonicalSyntaxDeltaEntries
+ (Interface.moduleSyntaxLocalDelta
+ (Parse.parsedModuleSyntaxInterface
+ (Parse.parsedWorkspaceRootModule
+ workspace)))))
+
+identifiesOwnerIndependentParsedModules :: Assertion
+identifiesOwnerIndependentParsedModules =
+ withTemporaryDirectory "felix-parsed-identity" \temp -> do
+ let firstRoot = temp Posix.</> "first"
+ secondRoot = temp Posix.</> "second"
+ bytes = axiomBlock "same" "x = x"
+ Directory.createDirectory firstRoot
+ Directory.createDirectory secondRoot
+ writeFile (firstRoot Posix.</> "entry.tex") bytes
+ writeFile (secondRoot Posix.</> "entry.tex") bytes
+ firstWorkspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph
+ =<< buildSearchedGraph firstRoot "entry.tex"
+ secondWorkspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph
+ =<< buildSearchedGraph secondRoot "entry.tex"
+ let first = Parse.parsedWorkspaceRootModule firstWorkspace
+ second = Parse.parsedWorkspaceRootModule secondWorkspace
+ assertBool
+ "logical owners remain distinct"
+ ( Module.moduleName (Parse.parsedModuleAddress first)
+ /= Module.moduleName (Parse.parsedModuleAddress second)
+ )
+ assertEqual
+ "equal bytes retain one content identity"
+ (Parse.parsedModuleSourceContentId first)
+ (Parse.parsedModuleSourceContentId second)
+ assertEqual
+ "physical source registration is outside parsed identity"
+ (Parse.parsedModuleId first)
+ (Parse.parsedModuleId second)
+ assertEqual
+ "canonical payload is owner-independent"
+ (Parse.parsedModulePayload first)
+ (Parse.parsedModulePayload second)
+ let payload = Parse.parsedModulePayload first
+ assertEqual
+ "canonical parsed payload cache round trip"
+ (Right payload)
+ (Cache.decodeCache
+ Parsed.getCanonicalParsedPayloadCache
+ (Cache.encodeCache
+ (Parsed.putCanonicalParsedPayloadCache payload)))
+ rebound <- expectRight
+ (Parsed.decodeCanonicalParsedPayload
+ (FileId 123)
+ payload)
+ case Parsed.decodedParsedBlocks rebound of
+ Raw.BlockAxiom location _title _marker _axiom : _ ->
+ assertEqual
+ "decoded locations bind only to the current live file"
+ (Just (FileId 123))
+ (locFileId location)
+ blocks ->
+ assertFailure
+ ("expected decoded axiom, got " <> show blocks)
+ writeFile
+ (secondRoot Posix.</> "entry.tex")
+ (bytes <> "% content identity change\n")
+ changedWorkspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph
+ =<< buildSearchedGraph secondRoot "entry.tex"
+ assertBool
+ "exact source changes parsed identity"
+ ( Parse.parsedModuleId first
+ /= Parse.parsedModuleId
+ (Parse.parsedWorkspaceRootModule changedWorkspace)
+ )
+
+keysEffectiveDirectSyntaxInputs :: Assertion
+keysEffectiveDirectSyntaxInputs =
+ withTemporaryDirectory "felix-parsed-syntax-input" \temp -> do
+ let firstRoot = temp Posix.</> "first"
+ secondRoot = temp Posix.</> "second"
+ rootBytes = "\\import{notation.tex}\n"
+ Directory.createDirectory firstRoot
+ Directory.createDirectory secondRoot
+ writeFile (firstRoot Posix.</> "entry.tex") rootBytes
+ writeFile (secondRoot Posix.</> "entry.tex") rootBytes
+ writeFile
+ (firstRoot Posix.</> "notation.tex")
+ (syntaxFunctionDefinition
+ "first_notation"
+ "firstop"
+ (Just "%! infixl 1"))
+ writeFile
+ (secondRoot Posix.</> "notation.tex")
+ (syntaxFunctionDefinition
+ "second_notation"
+ "secondop"
+ (Just "%! infixl 1"))
+ firstWorkspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph
+ =<< buildSearchedGraph firstRoot "entry.tex"
+ secondWorkspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph
+ =<< buildSearchedGraph secondRoot "entry.tex"
+ let first = Parse.parsedWorkspaceRootModule firstWorkspace
+ second = Parse.parsedWorkspaceRootModule secondWorkspace
+ assertEqual
+ "root source bytes are unchanged"
+ (Parse.parsedModuleSourceContentId first)
+ (Parse.parsedModuleSourceContentId second)
+ assertBool
+ "effective syntax changes the parsed key"
+ (Parse.parsedModuleKey first /= Parse.parsedModuleKey second)
+ assertBool
+ "effective syntax changes parsed identity"
+ (Parse.parsedModuleId first /= Parse.parsedModuleId second)
+
+reusesExactParsedSyntax :: Assertion
+reusesExactParsedSyntax =
+ withTemporaryDirectory "felix-parsed-warm" \temp -> do
+ let datatype = unlines
+ [ "\\begin{datatype}\\label{multi_item}"
+ , " Define $\\itemkind$ inductively as follows."
+ , " \\begin{enumerate}"
+ , " \\item $\\itemzero \\in \\itemkind$."
+ , " \\item $\\itemsucc{x} \\in \\itemkind$ for $x \\in \\itemkind$."
+ , " \\end{enumerate}"
+ , "\\end{datatype}"
+ ]
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (builtinZeroDefinition "source_zero" <> datatype)
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ foundation <- expectRight Foundation.checkedFoundation
+ store <- openTestStore
+ (temp Posix.</> "store.sqlite")
+ (Identity.theoryId foundation)
+ coldCallbacks <- newIORef (0 :: Int)
+ cold <- expectParseExecution
+ =<< Parse.parseSourceWorkspaceWithStoreAndSyntaxInputsAndCallback
+ store mounts request (const [])
+ (\_source _block -> modifyIORef' coldCallbacks (+ 1))
+ warmCallbacks <- newIORef (0 :: Int)
+ warm <- expectParseExecution
+ =<< Parse.parseSourceWorkspaceWithStoreAndSyntaxInputsAndCallback
+ store mounts request (const [])
+ (\_source _block -> modifyIORef' warmCallbacks (+ 1))
+ let coldRoot = Parse.parsedWorkspaceRootModule cold
+ warmRoot = Parse.parsedWorkspaceRootModule warm
+ let expectedChunkCount =
+ length (Parse.parsedModuleBlocks warmRoot)
+ assertBool "fixture has source chunks" (expectedChunkCount > 0)
+ assertEqual "cold callbacks" expectedChunkCount
+ =<< readIORef coldCallbacks
+ assertEqual "warm callbacks" expectedChunkCount
+ =<< readIORef warmCallbacks
+ assertEqual "warm blocks" (Parse.parsedModuleBlocks coldRoot)
+ (Parse.parsedModuleBlocks warmRoot)
+ assertEqual "warm occurrences"
+ (Parse.parsedModuleSyntaxOccurrences coldRoot)
+ (Parse.parsedModuleSyntaxOccurrences warmRoot)
+ assertEqual "warm syntax interface"
+ (Parse.parsedModuleSyntaxInterface coldRoot)
+ (Parse.parsedModuleSyntaxInterface warmRoot)
+ assertEqual "warm parsed identity"
+ (Parse.parsedModuleId coldRoot)
+ (Parse.parsedModuleId warmRoot)
+ case Parse.parsedModuleSyntaxOccurrences warmRoot of
+ first : second : third : fourth : [] -> do
+ assertEqual "fixed source marker" "source_zero"
+ (Parse.parsedSyntaxOccurrenceMarker first)
+ case Parse.parsedSyntaxOccurrenceEntry first of
+ Interface.CanonicalExpressionFunction
+ _pattern marker _fixity ->
+ assertEqual "fixed authoritative marker" "zero" marker
+ entry ->
+ assertFailure
+ ("unexpected fixed cached entry: " <> show entry)
+ assertEqual "multi-item block order" [1, 1, 1]
+ (Parse.parsedSyntaxOccurrenceBlockIndex
+ <$> [second, third, fourth])
+ assertEqual "multi-item scanner order"
+ ["multi_item", "itemzero", "itemsucc"]
+ (Parse.parsedSyntaxOccurrenceMarker
+ <$> [second, third, fourth])
+ case drop 1 (Parse.parsedModuleBlocks warmRoot) of
+ block : _ ->
+ case block of
+ Raw.BlockData _location _title marker _datatype ->
+ assertEqual "cached declaration-head anchor"
+ marker
+ (Parse.parsedSyntaxOccurrenceMarker second)
+ other ->
+ assertFailure
+ ("expected cached datatype block, got "
+ <> show other)
+ [] ->
+ assertFailure "cached datatype block is absent"
+ occurrences ->
+ assertFailure
+ ("unexpected cached syntax occurrences: "
+ <> show occurrences)
+ assertEqual "cold callback projection" 2
+ =<< readIORef coldCallbacks
+ assertEqual "warm callback projection" 2
+ =<< readIORef warmCallbacks
+ Store.closeStore store
+
+invalidatesExactParsedInputs :: Assertion
+invalidatesExactParsedInputs =
+ withTemporaryDirectory "felix-parsed-invalidation" \temp -> do
+ let notationPath = temp Posix.</> "notation.tex"
+ entryPath = temp Posix.</> "entry.tex"
+ notation associativity level =
+ syntaxFunctionDefinition
+ "join"
+ "join"
+ (Just
+ ("%! " <> associativity <> " " <> show level))
+ entry suffix =
+ "\\import{notation.tex}\n"
+ <> axiomBlock
+ "imported_syntax_use"
+ "a\\join b\\join c = a"
+ <> suffix
+ writeFile notationPath (notation "infixl" (1 :: Int))
+ writeFile entryPath (entry "")
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ foundation <- expectRight Foundation.checkedFoundation
+ store <- openTestStore
+ (temp Posix.</> "store.sqlite")
+ (Identity.theoryId foundation)
+ let parse =
+ expectParseExecution
+ =<< Parse.parseSourceWorkspaceWithStoreAndSyntaxInputs
+ store mounts request (const [])
+ coldWorkspace <- parse
+ warmWorkspace <- parse
+ coldNotation <- findParsedModule "notation.tex" coldWorkspace
+ warmNotation <- findParsedModule "notation.tex" warmWorkspace
+ let coldRoot = Parse.parsedWorkspaceRootModule coldWorkspace
+ warmRoot = Parse.parsedWorkspaceRootModule warmWorkspace
+ assertEqual "unchanged import identity"
+ (Parse.parsedModuleId coldNotation)
+ (Parse.parsedModuleId warmNotation)
+ assertEqual "unchanged importer identity"
+ (Parse.parsedModuleId coldRoot)
+ (Parse.parsedModuleId warmRoot)
+
+ writeFile entryPath (entry "% formatting-only edit\n")
+ editedWorkspace <- parse
+ editedNotation <- findParsedModule "notation.tex" editedWorkspace
+ let editedRoot = Parse.parsedWorkspaceRootModule editedWorkspace
+ assertEqual "cached import retains identity"
+ (Parse.parsedModuleId warmNotation)
+ (Parse.parsedModuleId editedNotation)
+ assertBool "exact source edit changes importer key"
+ (Parse.parsedModuleKey warmRoot
+ /= Parse.parsedModuleKey editedRoot)
+ assertEqual "formatting retains parsed projection"
+ (Parse.parsedModulePayload warmRoot)
+ (Parse.parsedModulePayload editedRoot)
+
+ writeFile notationPath (notation "infixr" (2 :: Int))
+ syntaxWorkspace <- parse
+ syntaxNotation <- findParsedModule "notation.tex" syntaxWorkspace
+ let syntaxRoot = Parse.parsedWorkspaceRootModule syntaxWorkspace
+ assertBool "local syntax identity changes"
+ ( Interface.moduleSyntaxAssertedId
+ (Parse.parsedModuleSyntaxInterface editedNotation)
+ /= Interface.moduleSyntaxAssertedId
+ (Parse.parsedModuleSyntaxInterface syntaxNotation)
+ )
+ assertEqual "importer source is unchanged"
+ (Parse.parsedModuleSourceContentId editedRoot)
+ (Parse.parsedModuleSourceContentId syntaxRoot)
+ assertBool "direct syntax invalidates importer key"
+ (Parse.parsedModuleKey editedRoot
+ /= Parse.parsedModuleKey syntaxRoot)
+ Store.closeStore store
+
+rebindsRelocatedParsedArtifacts :: Assertion
+rebindsRelocatedParsedArtifacts =
+ withTemporaryDirectory "felix-parsed-relocation" \temp -> do
+ let firstRoot = temp Posix.</> "first"
+ secondRoot = temp Posix.</> "second"
+ sourceBytes = axiomBlock "same" "x = x"
+ Directory.createDirectory firstRoot
+ Directory.createDirectory secondRoot
+ writeFile (firstRoot Posix.</> "entry.tex") sourceBytes
+ writeFile (secondRoot Posix.</> "entry.tex") sourceBytes
+ firstMounts <- oneMount "first" firstRoot
+ secondMounts <- oneMount "second" secondRoot
+ request <- expectRight (searchedRoot "entry.tex")
+ foundation <- expectRight Foundation.checkedFoundation
+ let theory = Identity.theoryId foundation
+ store <- openTestStore (temp Posix.</> "store.sqlite") theory
+ firstWorkspace <- expectParseExecution
+ =<< Parse.parseSourceWorkspaceWithStoreAndSyntaxInputs
+ store firstMounts request (const [])
+ secondWorkspace <- expectParseExecution
+ =<< Parse.parseSourceWorkspaceWithStoreAndSyntaxInputs
+ store secondMounts request (const [])
+ let first = Parse.parsedWorkspaceRootModule firstWorkspace
+ second = Parse.parsedWorkspaceRootModule secondWorkspace
+ firstSource = Parse.parsedModuleResolved first
+ secondSource = Parse.parsedModuleResolved second
+ assertEqual "relocation retains parsed identity"
+ (Parse.parsedModuleId first)
+ (Parse.parsedModuleId second)
+ assertEqual "relocation retains canonical payload"
+ (Parse.parsedModulePayload first)
+ (Parse.parsedModulePayload second)
+ assertBool "relocation rebinds the physical source"
+ (resolvedSourceCanonicalPath firstSource
+ /= resolvedSourceCanonicalPath secondSource)
+ assertBool "relocation rebinds the logical owner"
+ (Parse.parsedModuleAddress first
+ /= Parse.parsedModuleAddress second)
+ firstFileId <- expectJust "first location file id"
+ (locFileId (onlyAxiomLocation first))
+ secondFileId <- expectJust "second location file id"
+ (locFileId (onlyAxiomLocation second))
+ assertBool "relocation rebinds locations"
+ (firstFileId /= secondFileId)
+ firstArtifactKey <- expectRight
+ (Semantic.moduleArtifactKey
+ (Module.moduleName (Parse.parsedModuleAddress first))
+ (Parse.parsedModuleId first)
+ []
+ theory)
+ secondArtifactKey <- expectRight
+ (Semantic.moduleArtifactKey
+ (Module.moduleName (Parse.parsedModuleAddress second))
+ (Parse.parsedModuleId second)
+ []
+ theory)
+ assertBool "module artifact remains owner-dependent"
+ (Semantic.moduleArtifactId firstArtifactKey
+ /= Semantic.moduleArtifactId secondArtifactKey)
+ Store.closeStore store
+
+rejectsCorruptedCachedDeclarationAnchor :: Assertion
+rejectsCorruptedCachedDeclarationAnchor =
+ withTemporaryDirectory "felix-parsed-corrupt-anchor" \temp -> do
+ writeBuiltinZeroDefinition
+ (temp Posix.</> "entry.tex")
+ "source_zero"
+ mounts <- oneMount "project" temp
+ request <- expectRight (searchedRoot "entry.tex")
+ foundation <- expectRight Foundation.checkedFoundation
+ let storePath = temp Posix.</> "store.sqlite"
+ theory = Identity.theoryId foundation
+ store <- openTestStore storePath theory
+ cold <- expectParseExecution
+ =<< Parse.parseSourceWorkspaceWithStoreAndSyntaxInputs
+ store mounts request (const [])
+ let parsed = Parse.parsedWorkspaceRootModule cold
+ key = Parse.parsedModuleKey parsed
+ fileId <- case Parse.parsedModuleSyntaxOccurrences parsed of
+ occurrence : _ ->
+ expectJust
+ "parsed occurrence file id"
+ (locFileId
+ (Parse.parsedSyntaxOccurrenceLocation occurrence))
+ [] ->
+ assertFailure "parsed fixed occurrence is absent"
+ >> fail "unreachable"
+ decoded <- expectRight
+ (Parsed.decodeCanonicalParsedPayload
+ fileId
+ (Parse.parsedModulePayload parsed))
+ Store.closeStore store
+ let corruptedOccurrences = case Parsed.decodedParsedOccurrences decoded of
+ (blockIndex, location, _marker, entry) : rest ->
+ (blockIndex, location, "corrupted_anchor", entry) : rest
+ [] ->
+ []
+ corruptedPayload =
+ Parsed.canonicalParsedPayload
+ (Parsed.decodedParsedImports decoded)
+ (Parsed.decodedParsedBlocks decoded)
+ corruptedOccurrences
+ (Parsed.decodedParsedSyntaxInterface decoded)
+ corruptedId =
+ ParsedIdentity.parsedModuleId
+ key
+ (Parsed.canonicalParsedPayloadBytes corruptedPayload)
+ connection <- SQLite.open storePath
+ SQLite.execute connection
+ "UPDATE parsed_artifacts \
+ \SET parsed_module_id = ?, payload = ? \
+ \WHERE parsed_module_key = ?"
+ ( Cache.cacheDigestBytes
+ (ParsedIdentity.parsedModuleIdDigest corruptedId)
+ , Parsed.canonicalParsedPayloadBytes corruptedPayload
+ , Cache.cacheDigestBytes
+ (ParsedIdentity.parsedModuleKeyDigest key)
+ )
+ SQLite.close connection
+ current <- openTestStore storePath theory
+ callbacks <- newIORef (0 :: Int)
+ Parse.parseSourceWorkspaceWithStoreAndSyntaxInputsAndCallback
+ current mounts request (const [])
+ (\_source _block -> modifyIORef' callbacks (+ 1)) >>= \case
+ Left
+ (Parse.ParseExecutionArtifactIntegrityFailure
+ _source
+ (Parse.ParsedArtifactAssociationFailure
+ Parse.SyntaxOccurrenceMarkerMismatch{})) ->
+ pure ()
+ other ->
+ assertFailure
+ ("unexpected corrupted parsed result: " <> show other)
+ assertEqual "corrupt hit invokes no parse callback" 0
+ =<< readIORef callbacks
+ Store.closeStore current
+
+openTestStore :: FilePath -> Identity.TheoryId -> IO Store.Store
+openTestStore path theory =
+ Store.openStore path theory >>= \case
+ Left failure ->
+ assertFailure (show failure) >> fail "unreachable"
+ Right (_startup, store) ->
+ pure store
+
+expectParseExecution
+ :: Either Parse.ParseExecutionError value
+ -> IO value
+expectParseExecution = \case
+ Left failure ->
+ assertFailure (show failure) >> fail "unreachable"
+ Right value ->
+ pure value
+
+parsesSourceGraph :: Assertion
+parsesSourceGraph =
+ withTemporaryDirectory "felix-source-parse" \temp -> do
+ writeTheory (temp Posix.</> "shared.tex") [] "shared"
+ writeTheory (temp Posix.</> "entry.tex") ["shared.tex"] "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ emittedRef <- newIORef []
+ workspace <- expectRight =<<
+ Parse.parseResolvedSourceGraphWith graph
+ (\source _block ->
+ modifyIORef'
+ emittedRef
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source) :))
+ assertEqual "two parsed source nodes" 2
+ (length (Parse.parsedWorkspaceModules workspace))
+ assertEqual "one source-local block per node" [1, 1]
+ (toList
+ (length . Parse.parsedModuleBlocks
+ <$> Parse.parsedWorkspaceImportedBeforeImporter workspace))
+ assertEqual "imported-before-importer source order"
+ ["shared.tex", "entry.tex"]
+ (toList
+ (safeRelativePathFilePath
+ . resolvedSourceRelativePath
+ . Parse.parsedModuleResolved
+ <$> Parse.parsedWorkspaceImportedBeforeImporter workspace))
+ assertEqual "flattened block view" 2
+ (length (Parse.importedBeforeImporterBlocks workspace))
+ emitted <- reverse <$> readIORef emittedRef
+ assertEqual "streamed block order"
+ ["shared.tex", "entry.tex"]
+ emitted
+
+rejectsSiblingSyntaxLeakage :: Assertion
+rejectsSiblingSyntaxLeakage =
+ withTemporaryDirectory "felix-source-syntax-world" \temp -> do
+ writeFile
+ (temp Posix.</> "use.tex")
+ (unlines
+ [ "\\begin{axiom}\\label{use}"
+ , " $x$ is special."
+ , "\\end{axiom}"
+ ])
+ writeAdjectiveDefinition
+ (temp Posix.</> "declare.tex")
+ "shared_special"
+ writeTheory
+ (temp Posix.</> "entry.tex")
+ ["use.tex", "declare.tex"]
+ "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ result <- Parse.parseResolvedSourceGraph graph
+ case result of
+ Left (Parse.SourceParseError source _parseError) ->
+ assertEqual
+ "syntax consumer fails in its own module"
+ "use.tex"
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source))
+ Left err ->
+ assertFailure
+ ("expected a source-local parse error, got "
+ <> show err)
+ Right workspace ->
+ assertFailure
+ ("sibling syntax leaked into use.tex: "
+ <> show workspace)
+
+parsesSourceFixities :: Assertion
+parsesSourceFixities =
+ withTemporaryDirectory "felix-source-fixity" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ ( syntaxFunctionDefinition
+ "loose"
+ "loose"
+ (Just "%! infixl 0")
+ <> syntaxFunctionDefinition
+ "tight"
+ "tight"
+ (Just "%! infixr 7")
+ <> axiomBlock
+ "loose_associativity"
+ "a\\loose b\\loose c = a"
+ <> axiomBlock
+ "tight_associativity"
+ "a\\tight b\\tight c = a"
+ <> axiomBlock
+ "mixed_precedence"
+ "a\\loose b\\tight c = a"
+ <> axiomBlock
+ "parenthesized_precedence"
+ "(a\\loose b)\\tight c = a"
+ )
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ let root =
+ Parse.parsedWorkspaceRootModule workspace
+ localEntries =
+ Interface.canonicalSyntaxDeltaEntries
+ (Interface.moduleSyntaxLocalDelta
+ (Parse.parsedModuleSyntaxInterface root))
+ assertExpressionFixity
+ "loose"
+ Raw.LeftAssoc
+ 0
+ localEntries
+ assertExpressionFixity
+ "tight"
+ Raw.RightAssoc
+ 7
+ localEntries
+ assertEqual
+ "source declaration occurrences"
+ [0, 1]
+ (Parse.parsedSyntaxOccurrenceBlockIndex
+ <$> Parse.parsedModuleSyntaxOccurrences root)
+ case drop 2 (Parse.parsedModuleBlocks root) of
+ [ looseAssociativity
+ , tightAssociativity
+ , mixedPrecedence
+ , parenthesizedPrecedence
+ ] -> do
+ assertAxiomLeftShape
+ "left associativity"
+ "loose(loose(a,b),c)"
+ looseAssociativity
+ assertAxiomLeftShape
+ "right associativity"
+ "tight(a,tight(b,c))"
+ tightAssociativity
+ assertAxiomLeftShape
+ "mixed precedence"
+ "loose(a,tight(b,c))"
+ mixedPrecedence
+ assertAxiomLeftShape
+ "parentheses override precedence"
+ "tight(loose(a,b),c)"
+ parenthesizedPrecedence
+ blocks ->
+ assertFailure
+ ("expected four fixity axioms, got "
+ <> show blocks)
+
+parsesLibraryFixities :: Assertion
+parsesLibraryFixities =
+ withTemporaryDirectory "felix-source-library-fixity" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ ( syntaxFunctionDefinition
+ "cdot"
+ "cdot"
+ (Just "%! infixl 4")
+ <> syntaxFunctionDefinition
+ "symdiff"
+ "symdiff"
+ (Just "%! infixl 1")
+ <> axiomBlock
+ "cdot_associativity"
+ "a\\cdot b\\cdot c = a"
+ <> axiomBlock
+ "symdiff_associativity"
+ "a\\symdiff b\\symdiff c = a"
+ <> axiomBlock
+ "library_mixed_precedence"
+ "a\\symdiff b\\cdot c = a"
+ <> axiomBlock
+ "library_parentheses"
+ "(a\\symdiff b)\\cdot c = a"
+ )
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ case drop 2
+ (Parse.parsedModuleBlocks
+ (Parse.parsedWorkspaceRootModule workspace)) of
+ [ cdotAssociativity
+ , symdiffAssociativity
+ , mixedPrecedence
+ , parenthesizedPrecedence
+ ] -> do
+ assertAxiomLeftShape
+ "cdot left associativity"
+ "cdot(cdot(a,b),c)"
+ cdotAssociativity
+ assertAxiomLeftShape
+ "symdiff left associativity"
+ "symdiff(symdiff(a,b),c)"
+ symdiffAssociativity
+ assertAxiomLeftShape
+ "cdot binds tighter than symdiff"
+ "symdiff(a,cdot(b,c))"
+ mixedPrecedence
+ assertAxiomLeftShape
+ "library parentheses override precedence"
+ "cdot(symdiff(a,b),c)"
+ parenthesizedPrecedence
+ blocks ->
+ assertFailure
+ ("expected four library-fixity axioms, got "
+ <> show blocks)
+
+validatesSourcePragmaAssociations :: Assertion
+validatesSourcePragmaAssociations =
+ forM_ cases \(description, contents, checkProblem) ->
+ withTemporaryDirectory
+ ("felix-source-pragma-" <> description)
+ \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ contents
+ graph <- buildSearchedGraph temp "entry.tex"
+ result <- Parse.parseResolvedSourceGraph graph
+ case result of
+ Left
+ (Parse.SourceSyntaxDeclarationError
+ source
+ problem) -> do
+ assertEqual
+ "pragma source"
+ "entry.tex"
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source))
+ checkProblem problem
+ Left err ->
+ assertFailure
+ ("expected source pragma error, got "
+ <> show err)
+ Right workspace ->
+ assertFailure
+ ("expected source pragma rejection, got "
+ <> show workspace)
+ where
+ cases
+ :: [ ( String
+ , String
+ , Parse.SyntaxDeclarationError -> Assertion
+ )
+ ]
+ cases =
+ [ ( "outside"
+ , "%! infixl 1\n" <> theoryBlock "outside"
+ , \case
+ Parse.SyntaxPragmaOutsideDeclaration{} ->
+ pure ()
+ problem ->
+ unexpected "outside-declaration pragma" problem
+ )
+ , ( "inside-nonsyntax"
+ , unlines
+ [ "\\begin{axiom}\\label{inside_nonsyntax}"
+ , " %! infixl 1"
+ , " $x = x$."
+ , "\\end{axiom}"
+ ]
+ , \case
+ Parse.SyntaxPragmaOutsideDeclaration location ->
+ assertEqual
+ "pragma in non-syntax chunk"
+ 2
+ (locLine location)
+ problem ->
+ unexpected "non-syntax declaration pragma" problem
+ )
+ , ( "missing"
+ , syntaxFunctionDefinition
+ "missing"
+ "missing"
+ Nothing
+ , \case
+ Parse.MissingSyntaxPragma{} ->
+ pure ()
+ problem ->
+ unexpected "missing pragma" problem
+ )
+ , ( "duplicate"
+ , unlines
+ [ "\\begin{abbreviation}\\label{duplicate}"
+ , " %! infixl 1"
+ , " %! infixl 1"
+ , " $x\\duplicate y = x$."
+ , "\\end{abbreviation}"
+ ]
+ , \case
+ Parse.DuplicateSyntaxPragma{} ->
+ pure ()
+ problem ->
+ unexpected "duplicate pragma" problem
+ )
+ , ( "irrelevant"
+ , unlines
+ [ "\\begin{definition}\\label{irrelevant}"
+ , " %! infixl 1"
+ , " $x$ is irrelevant iff $x = x$."
+ , "\\end{definition}"
+ ]
+ , \case
+ Parse.IrrelevantSyntaxPragma{} ->
+ pure ()
+ problem ->
+ unexpected "irrelevant pragma" problem
+ )
+ , ( "multiple-without-pragma"
+ , unlines
+ [ "\\begin{datatype}\\label{multiple_patterns}"
+ , " Define $\\patternkind$ inductively as follows."
+ , " \\begin{enumerate}"
+ , " \\item $(x \\firstpattern y) \\in \\patternkind$."
+ , " \\item $(x \\secondpattern y) \\in \\patternkind$."
+ , " \\end{enumerate}"
+ , "\\end{datatype}"
+ ]
+ , \case
+ problem@(Parse.MultipleNewSyntaxPatternsWithoutPragma
+ location
+ patterns) -> do
+ assertEqual
+ "first new pattern location"
+ 4
+ (locLine location)
+ assertEqual
+ "new pattern count"
+ 2
+ (NonEmpty.length patterns)
+ assertBool
+ "accurate multiple-pattern message"
+ ("several new eligible patterns that V1 cannot select between"
+ `List.isInfixOf` show problem)
+ assertBool
+ "message requires an unambiguous declaration"
+ ("make the declaration unambiguous"
+ `List.isInfixOf` show problem)
+ problem ->
+ unexpected "multiple unannotated patterns" problem
+ )
+ , ( "fixed"
+ , unlines
+ [ "\\begin{abbreviation}\\label{local_addition}"
+ , " %! infixl 1"
+ , " $x + y = x$."
+ , "\\end{abbreviation}"
+ ]
+ , \case
+ Parse.SyntaxPragmaOnFixedReuse{} ->
+ pure ()
+ problem ->
+ unexpected "fixed-base pragma" problem
+ )
+ ]
+
+ unexpected expected problem =
+ assertFailure
+ ("expected " <> expected <> ", got " <> show problem)
+
+rejectsFixedBaseCategoryMismatch :: Assertion
+rejectsFixedBaseCategoryMismatch =
+ withTemporaryDirectory "felix-source-fixed-category" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\begin{definition}\\label{local_add_relation}"
+ , " $x + y$ iff $x = y$."
+ , "\\end{definition}"
+ ])
+ graph <- buildSearchedGraph temp "entry.tex"
+ collision <- expectLexiconCollision
+ =<< Parse.parseResolvedSourceGraph graph
+ assertEqual
+ "fixed collision pattern"
+ (Raw.HoleCons
+ (Raw.TokenCons
+ (Raw.Symbol "+")
+ (Raw.HoleCons Raw.End)))
+ (Parse.lexiconCollisionPattern collision)
+ case toList (Parse.lexiconCollisionOrigins collision) of
+ [ Parse.FixedLexiconOrigin
+ Interface.CanonicalExpressionFunction{}
+ , Parse.SourceLexiconOrigin
+ Interface.CanonicalRelation{}
+ source
+ location
+ ] -> do
+ assertEqual
+ "local collision source"
+ "entry.tex"
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source))
+ assertLocation
+ "local collision declaration"
+ "entry.tex"
+ 1
+ location
+ origins ->
+ assertFailure
+ ("expected fixed/source category origins, got "
+ <> show origins)
+
+retainsMultiItemSyntaxOccurrences :: Assertion
+retainsMultiItemSyntaxOccurrences =
+ withTemporaryDirectory "felix-source-multi-item-syntax" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\begin{datatype}\\label{multi_item}"
+ , " Define $\\itemkind$ inductively as follows."
+ , " \\begin{enumerate}"
+ , " \\item $\\itemzero \\in \\itemkind$."
+ , " \\item $\\itemsucc{x} \\in \\itemkind$ for $x \\in \\itemkind$."
+ , " \\end{enumerate}"
+ , "\\end{datatype}"
+ ])
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ let root =
+ Parse.parsedWorkspaceRootModule workspace
+ occurrences =
+ Parse.parsedModuleSyntaxOccurrences root
+ summarize occurrence =
+ case Parse.parsedSyntaxOccurrenceEntry occurrence of
+ Interface.CanonicalExpressionFunction
+ _pattern
+ marker
+ _fixity ->
+ Right
+ ( Parse.parsedSyntaxOccurrenceBlockIndex
+ occurrence
+ , locLine
+ (Parse.parsedSyntaxOccurrenceLocation
+ occurrence)
+ , Parse.parsedSyntaxOccurrenceMarker occurrence
+ , marker
+ )
+ entry ->
+ Left entry
+ case traverse summarize occurrences of
+ Right summaries ->
+ assertEqual
+ "block association and scanner order"
+ [ (0, 2, "multi_item", "multi_item")
+ , (0, 4, "itemzero", "itemzero")
+ , (0, 5, "itemsucc", "itemsucc")
+ ]
+ summaries
+ Left entry ->
+ assertFailure
+ ("expected an expression occurrence, got "
+ <> show entry)
+ case (Parse.parsedModuleBlocks root, occurrences) of
+ ( Raw.BlockData _location _title blockMarker _datatype : _
+ , firstOccurrence : _
+ ) ->
+ assertEqual
+ "first occurrence is the declaration-head anchor"
+ blockMarker
+ (Parse.parsedSyntaxOccurrenceMarker firstOccurrence)
+ _ ->
+ assertFailure "expected a datatype block and its occurrences"
+
+propagatesImportedSyntax :: Assertion
+propagatesImportedSyntax =
+ withTemporaryDirectory "felix-source-syntax-diamond" \temp -> do
+ writeFile
+ (temp Posix.</> "base.tex")
+ (syntaxFunctionDefinition
+ "star"
+ "star"
+ (Just "%! infixl 3"))
+ writeFile
+ (temp Posix.</> "left.tex")
+ ("\\import{base.tex}\n"
+ <> syntaxFunctionDefinition
+ "star"
+ "star"
+ Nothing)
+ writeTheory
+ (temp Posix.</> "right.tex")
+ ["base.tex"]
+ "right"
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\import{left.tex}"
+ , "\\import{right.tex}"
+ ]
+ <> axiomBlock
+ "imported_use"
+ "a\\star b\\star c = a")
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ baseModule <- findParsedModule "base.tex" workspace
+ leftModule <- findParsedModule "left.tex" workspace
+ rightModule <- findParsedModule "right.tex" workspace
+ let root =
+ Parse.parsedWorkspaceRootModule workspace
+ interface =
+ Parse.parsedModuleSyntaxInterface
+ localEntries parsed =
+ Interface.canonicalSyntaxDeltaEntries
+ (Interface.moduleSyntaxLocalDelta
+ (interface parsed))
+ assertEqual "base exports one syntax entry"
+ 1
+ (length (localEntries baseModule))
+ assertEqual "imported reuse emits no local entry"
+ []
+ (localEntries leftModule)
+ assertEqual "imported reuse retains its occurrence"
+ 1
+ (length
+ (Parse.parsedModuleSyntaxOccurrences leftModule))
+ assertEqual "empty diamond branch has no occurrence"
+ []
+ (Parse.parsedModuleSyntaxOccurrences rightModule)
+ assertEqual "equal diamond interfaces"
+ (Interface.moduleSyntaxAssertedId
+ (interface leftModule))
+ (Interface.moduleSyntaxAssertedId
+ (interface rightModule))
+ assertEqual "root coalesces equal direct interfaces"
+ 1
+ (length
+ (Interface.moduleSyntaxDirectInputs
+ (interface root)))
+ case Parse.parsedModuleBlocks root of
+ [block] ->
+ assertAxiomLeftShape
+ "imported left associativity"
+ "star(star(a,b),c)"
+ block
+ blocks ->
+ assertFailure
+ ("expected one imported-syntax axiom, got "
+ <> show blocks)
+ writeFile
+ (temp Posix.</> "left.tex")
+ ("\\import{base.tex}\n"
+ <> syntaxFunctionDefinition
+ "star"
+ "star"
+ (Just "%! infixl 3"))
+ reuseGraph <- buildSearchedGraph temp "left.tex"
+ reuseResult <-
+ Parse.parseResolvedSourceGraph reuseGraph
+ case reuseResult of
+ Left
+ (Parse.SourceSyntaxDeclarationError
+ _source
+ Parse.SyntaxPragmaOnImportedReuse{}) ->
+ pure ()
+ Left err ->
+ assertFailure
+ ("expected imported-reuse pragma rejection, got "
+ <> show err)
+ Right reused ->
+ assertFailure
+ ("expected imported-reuse pragma rejection, got "
+ <> show reused)
+
+rejectsUnequalImportedSyntax :: Assertion
+rejectsUnequalImportedSyntax =
+ forM_ cases \(description, leftDefinition, rightDefinition) ->
+ withTemporaryDirectory
+ ("felix-source-imported-collision-" <> description)
+ \temp -> do
+ writeFile
+ (temp Posix.</> "a.tex")
+ leftDefinition
+ writeFile
+ (temp Posix.</> "b.tex")
+ rightDefinition
+ writeTheory
+ (temp Posix.</> "entry.tex")
+ ["a.tex", "b.tex"]
+ "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ collision <- expectLexiconCollision
+ =<< Parse.parseResolvedSourceGraph graph
+ (firstLocation, secondLocation) <-
+ expectTwoCollisionLocations collision
+ assertLocation
+ "first imported declaration"
+ "a.tex"
+ 1
+ firstLocation
+ assertLocation
+ "second imported declaration"
+ "b.tex"
+ 1
+ secondLocation
+ where
+ cases =
+ [ ( "marker"
+ , syntaxFunctionDefinition
+ "clash_left"
+ "clash"
+ (Just "%! infixl 2")
+ , syntaxFunctionDefinition
+ "clash_right"
+ "clash"
+ (Just "%! infixl 2")
+ )
+ , ( "fixity"
+ , syntaxFunctionDefinition
+ "clash"
+ "clash"
+ (Just "%! infixl 2")
+ , syntaxFunctionDefinition
+ "clash"
+ "clash"
+ (Just "%! infixr 2")
+ )
+ ]
+
+distinguishesPhysicalSourceLocations :: Assertion
+distinguishesPhysicalSourceLocations =
+ withTemporaryDirectory "felix-source-location-identity" \temp -> do
+ let projectRoot = temp Posix.</> "project"
+ libraryRoot = temp Posix.</> "library"
+ projectEntry = projectRoot Posix.</> "entry.tex"
+ libraryEntry = libraryRoot Posix.</> "entry.tex"
+ Directory.createDirectory projectRoot
+ Directory.createDirectory libraryRoot
+ writeFile projectEntry
+ ("\\import{entry.tex}\n"
+ <> adjectiveDefinition "project_adjective")
+ writeNounDefinition libraryEntry "library_noun"
+ mounts <- expectRight =<< prepareSourceMounts
+ [ (sourceMountId "library", libraryRoot)
+ , (sourceMountId "project", projectRoot)
+ ]
+ request <- expectRight =<< existingRoot projectEntry
+ graph <- expectRight =<< buildResolvedSourceGraph mounts request
+ collision <- expectLexiconCollision
+ =<< Parse.parseResolvedSourceGraph graph
+ assertEqual "normalized cross-category pattern"
+ (Raw.TokenCons (Raw.Word "special") Raw.End)
+ (Parse.lexiconCollisionPattern collision)
+ (libraryLocation, projectLocation) <-
+ expectTwoCollisionLocations collision
+ assertEqual "accepted display path"
+ "entry.tex"
+ (locFile libraryLocation)
+ assertEqual "accepted declaration line"
+ 1
+ (locLine libraryLocation)
+ assertEqual "colliding display path"
+ "entry.tex"
+ (locFile projectLocation)
+ assertEqual "colliding declaration line"
+ 2
+ (locLine projectLocation)
+ canonicalProject <- Directory.canonicalizePath projectEntry
+ canonicalLibrary <- Directory.canonicalizePath libraryEntry
+ let rendered = show collision
+ quotedLibrary = show canonicalLibrary
+ quotedProject = show canonicalProject
+ assertBool "rendered error includes accepted canonical path"
+ (quotedLibrary `List.isInfixOf` rendered)
+ assertBool "rendered error includes colliding canonical path"
+ (quotedProject `List.isInfixOf` rendered)
+ assertBool "canonical locations render in declaration order"
+ (substringIndex quotedLibrary rendered
+ < substringIndex quotedProject rendered)
+
+retainsWorkspaceLocationDisplayPath :: Assertion
+retainsWorkspaceLocationDisplayPath =
+ withTemporaryDirectory "felix-source-location-display" \temp -> do
+ let nested = temp Posix.</> "nested"
+ entry = nested Posix.</> "entry.tex"
+ Directory.createDirectory nested
+ writeTheory entry [] "entry"
+ outerMounts <- oneMount "project" temp
+ outerRequest <- expectRight (searchedRoot "nested/entry.tex")
+ outerGraph <- expectRight =<<
+ buildResolvedSourceGraph outerMounts outerRequest
+ outerWorkspace <- expectRight =<<
+ Parse.parseResolvedSourceGraph outerGraph
+ innerMounts <- oneMount "library" nested
+ innerRequest <- expectRight (searchedRoot "entry.tex")
+ innerGraph <- expectRight =<<
+ buildResolvedSourceGraph innerMounts innerRequest
+ innerWorkspace <- expectRight =<<
+ Parse.parseResolvedSourceGraph innerGraph
+ let outerLocation =
+ onlyAxiomLocation
+ (Parse.parsedWorkspaceRootModule outerWorkspace)
+ innerLocation =
+ onlyAxiomLocation
+ (Parse.parsedWorkspaceRootModule innerWorkspace)
+ assertEqual "outer-mount display path"
+ "nested/entry.tex"
+ (locFile outerLocation)
+ assertEqual "more-specific-mount display path"
+ "entry.tex"
+ (locFile innerLocation)
+ outerFileId <- expectJust "outer workspace file id"
+ (locFileId outerLocation)
+ innerFileId <- expectJust "inner workspace file id"
+ (locFileId innerLocation)
+ assertBool "distinct display registrations use distinct file ids"
+ (outerFileId /= innerFileId)
+ canonicalEntry <- Directory.canonicalizePath entry
+ assertEqual "outer physical location key"
+ (Just canonicalEntry)
+ (lookupFileIdentityPath outerFileId)
+ assertEqual "inner physical location key"
+ (Just canonicalEntry)
+ (lookupFileIdentityPath innerFileId)
+
+reportsImportedScannerErrorFirst :: Assertion
+reportsImportedScannerErrorFirst =
+ withTemporaryDirectory "felix-source-lexer-error-order" \temp -> do
+ let scannerFailure = unlines
+ [ "\\begin{abbreviation}\\label{malformed_function}"
+ , " $x = \\emptyset$."
+ , "\\end{abbreviation}"
+ ]
+ tokenizerFailure = unlines
+ [ "\\begin{axiom}"
+ , "#"
+ , "\\end{axiom}"
+ ]
+ writeFile
+ (temp Posix.</> "imported.tex")
+ scannerFailure
+ writeFile
+ (temp Posix.</> "entry.tex")
+ ("\\import{imported.tex}\n" <> tokenizerFailure)
+ graph <- buildSearchedGraph temp "entry.tex"
+ result <- Parse.parseResolvedSourceGraph graph
+ case result of
+ Left
+ (Parse.SourceParseError
+ source
+ (Parse.LexicalScanFailure
+ Adapt.InvalidFunctionPattern{})) ->
+ assertEqual
+ "dependency scanner error"
+ "imported.tex"
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source))
+ Left err ->
+ assertFailure
+ ("expected imported scanner error, got " <> show err)
+ Right workspace ->
+ assertFailure
+ ("expected imported scanner error, got "
+ <> show workspace)
+
+reportsMalformedLexicalDeclaration :: Assertion
+reportsMalformedLexicalDeclaration =
+ withTemporaryDirectory "felix-source-malformed-lexical" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\begin{abbreviation}\\label{malformed_function}"
+ , " $x = \\emptyset$."
+ , "\\end{abbreviation}"
+ ])
+ graph <- buildSearchedGraph temp "entry.tex"
+ result <- Parse.parseResolvedSourceGraph graph
+ void (evaluate (length (show result)))
+ case result of
+ Left
+ (Parse.SourceParseError
+ source
+ (Parse.LexicalScanFailure
+ (Adapt.InvalidFunctionPattern
+ location
+ Adapt.FunctionPatternBareVariable))) -> do
+ assertEqual "malformed source"
+ "entry.tex"
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source))
+ assertLocation
+ "malformed declaration"
+ "entry.tex"
+ 1
+ location
+ Left err ->
+ assertFailure
+ ("expected typed lexical scan failure, got " <> show err)
+ Right workspace ->
+ assertFailure
+ ("expected typed lexical scan failure, got "
+ <> show workspace)
+
+rejectsMalformedInductivePattern :: Assertion
+rejectsMalformedInductivePattern =
+ case runLexer (FileId 0) "inductive.tex" source of
+ Left err ->
+ assertFailure ("could not tokenize fixture: " <> show err)
+ Right (_imports, [chunk]) ->
+ case Adapt.scanChunk chunk of
+ Left
+ (Adapt.InvalidFunctionPattern
+ _location
+ Adapt.FunctionPatternBareVariable) ->
+ pure ()
+ Left err ->
+ assertFailure
+ ("expected bare-variable scan failure, got "
+ <> show err)
+ Right scans ->
+ assertFailure
+ ("expected bare-variable scan failure, got "
+ <> show scans)
+ Right (_imports, chunks) ->
+ assertFailure
+ ("expected one lexical chunk, got " <> show (length chunks))
+ where
+ source =
+ Text.pack
+ (unlines
+ [ "\\begin{inductive}\\label{malformed_inductive}"
+ , " Define $x\\subseteq\\pow{x}$ inductively."
+ , "\\end{inductive}"
+ ])
+
+acceptsAdjectiveSignature :: Assertion
+acceptsAdjectiveSignature =
+ withTemporaryDirectory "felix-source-signature-adjective" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\begin{signature}\\label{reflexive_signature}"
+ , " Suppose $A$ is a set."
+ , " Then $x$ can be reflexive."
+ , "\\end{signature}"
+ , "\\begin{axiom}\\label{reflexive_use}"
+ , " $x$ is reflexive."
+ , "\\end{axiom}"
+ ])
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ let blocks = Parse.importedBeforeImporterBlocks workspace
+ case blocks of
+ [ Raw.BlockSig
+ _signatureLocation
+ _signatureTitle
+ _signatureMarker
+ [_signatureAssumption]
+ (Raw.SignatureAdj
+ _variable
+ (Raw.Adj _adjectiveLocation declaredAdjective []))
+ , Raw.BlockAxiom{}
+ ] -> do
+ assertEqual
+ "signature marker enters the lexicon"
+ "reflexive_signature"
+ (Raw.lexicalItemMarker declaredAdjective)
+ _ ->
+ assertFailure
+ ("unexpected adjective-signature blocks: " <> show blocks)
+
+rejectsMalformedSignatureHead :: Assertion
+rejectsMalformedSignatureHead = do
+ case runLexer
+ (FileId 49)
+ "malformed-signature.tex"
+ (Text.unlines
+ [ "\\begin{signature}\\label{bad_signature}"
+ , " $x$ can be."
+ , "\\end{signature}"
+ ]) of
+ Left err ->
+ assertFailure ("unexpected token error: " <> show err)
+ Right (_imports, [chunk]) ->
+ case Adapt.scanChunk chunk of
+ Left
+ (Adapt.InvalidFunctionPattern
+ location
+ Adapt.FunctionPatternBareVariable) -> do
+ assertEqual "error line" 1 (locLine location)
+ assertEqual "error column" 1 (locColumn location)
+ Left err ->
+ assertFailure
+ ("expected malformed signature error, got " <> show err)
+ Right scans ->
+ assertFailure
+ ("expected malformed signature rejection, got "
+ <> show scans)
+ Right (_imports, chunks) ->
+ assertFailure
+ ("expected one malformed signature chunk, got "
+ <> show (length chunks))
+
+reportsSameSourceLexiconCollision :: Assertion
+reportsSameSourceLexiconCollision =
+ withTemporaryDirectory "felix-source-local-lexicon-collision" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\begin{struct}\\label{duplicate_operations}"
+ , " A \\duplicateop $X$ is equipped with"
+ , " \\begin{enumerate}"
+ , " \\item $\\duplicateop$"
+ , " \\end{enumerate}"
+ , "\\end{struct}"
+ ])
+ graph <- buildSearchedGraph temp "entry.tex"
+ collision <- expectLexiconCollision
+ =<< Parse.parseResolvedSourceGraph graph
+ (firstLocation, secondLocation) <-
+ expectTwoCollisionLocations collision
+ assertEqual "first declaration file"
+ "entry.tex"
+ (locFile firstLocation)
+ assertEqual "first declaration line" 1 (locLine firstLocation)
+ assertEqual "colliding declaration file"
+ "entry.tex"
+ (locFile secondLocation)
+ assertEqual "colliding declaration line" 4 (locLine secondLocation)
+ assertBool "declarations have distinct locations"
+ (firstLocation /= secondLocation)
+
+acceptsBuiltinSourceDeclaration :: Assertion
+acceptsBuiltinSourceDeclaration =
+ withTemporaryDirectory "felix-source-builtin-declaration" \temp -> do
+ writeBuiltinZeroDefinition
+ (temp Posix.</> "entry.tex")
+ "source_zero"
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ let root =
+ Parse.parsedWorkspaceRootModule workspace
+ assertEqual
+ "fixed reuse emits no local syntax"
+ []
+ (Interface.canonicalSyntaxDeltaEntries
+ (Interface.moduleSyntaxLocalDelta
+ (Parse.parsedModuleSyntaxInterface root)))
+ case Parse.parsedModuleBlocks root of
+ [Raw.BlockAbbr
+ _location
+ _title
+ blockMarker
+ (Raw.AbbreviationEq
+ (Raw.SymbolPattern
+ (Raw.MixfixItem
+ _pattern
+ symbolMarker
+ _associativity)
+ [])
+ _expression)] -> do
+ assertEqual
+ "declaration label remains independent"
+ "source_zero"
+ blockMarker
+ assertEqual
+ "built-in marker remains authoritative"
+ "zero"
+ symbolMarker
+ case Parse.parsedModuleSyntaxOccurrences root of
+ [occurrence] ->
+ case Parse.parsedSyntaxOccurrenceEntry occurrence of
+ Interface.CanonicalExpressionFunction
+ _pattern
+ occurrenceMarker
+ _fixity -> do
+ assertEqual
+ "occurrence retains source marker"
+ "source_zero"
+ (Parse.parsedSyntaxOccurrenceMarker
+ occurrence)
+ assertEqual
+ "occurrence uses fixed marker"
+ "zero"
+ occurrenceMarker
+ fileId <- expectJust
+ "fixed occurrence file id"
+ (locFileId
+ (Parse.parsedSyntaxOccurrenceLocation
+ occurrence))
+ decoded <- expectRight
+ (Parsed.decodeCanonicalParsedPayload
+ fileId
+ (Parse.parsedModulePayload root))
+ case Parsed.decodedParsedOccurrences decoded of
+ [ ( _blockIndex
+ , _location
+ , storedMarker
+ , Interface.CanonicalExpressionFunction
+ _storedPattern
+ storedEntryMarker
+ _storedFixity
+ )
+ ] -> do
+ assertEqual
+ "payload source marker"
+ "source_zero"
+ storedMarker
+ assertEqual
+ "payload authoritative marker"
+ "zero"
+ storedEntryMarker
+ stored ->
+ assertFailure
+ ("unexpected decoded fixed occurrence: "
+ <> show stored)
+ entry ->
+ assertFailure
+ ("unexpected fixed occurrence: "
+ <> show entry)
+ occurrences ->
+ assertFailure
+ ("unexpected fixed occurrences: "
+ <> show occurrences)
+ blocks ->
+ assertFailure
+ ("unexpected built-in declaration parse: "
+ <> show blocks)
+
+acceptsBuiltinPrefixPredicateDeclaration :: Assertion
+acceptsBuiltinPrefixPredicateDeclaration =
+ withTemporaryDirectory "felix-source-builtin-prefix" \temp -> do
+ writeBuiltinCongDefinition
+ (temp Posix.</> "entry.tex")
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ case Parse.parsedModuleBlocks
+ (Parse.parsedWorkspaceRootModule workspace) of
+ [Raw.BlockDefn
+ _location
+ _title
+ _blockMarker
+ (Raw.Defn
+ _assumptions
+ (Raw.DefnSymbolicPredicate
+ predicate
+ predicateMarker
+ _variables)
+ _statement)] -> do
+ assertEqual "built-in prefix predicate"
+ (Raw.PrefixPredicate "Cong" 4)
+ predicate
+ assertEqual
+ "built-in prefix marker remains authoritative"
+ "cong"
+ predicateMarker
+ blocks ->
+ assertFailure
+ ("unexpected built-in prefix declaration parse: "
+ <> show blocks)
+
+avoidsAliasImportLexiconCollision :: Assertion
+avoidsAliasImportLexiconCollision =
+ withTemporaryDirectory "felix-source-alias-lexicon" \temp -> do
+ let shared = temp Posix.</> "shared.tex"
+ alias = temp Posix.</> "alias.tex"
+ writeAdjectiveDefinition shared "shared_special"
+ Directory.createFileLink shared alias
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (unlines
+ [ "\\import{shared.tex}"
+ , "\\import{shared.tex}"
+ , "\\import{alias.tex}"
+ , "\\begin{axiom}\\label{root}"
+ , " $x$ is special."
+ , "\\end{axiom}"
+ ])
+ graph <- buildSearchedGraph temp "entry.tex"
+ workspace <- expectRight
+ =<< Parse.parseResolvedSourceGraph graph
+ assertEqual "canonical source is parsed once"
+ ["shared.tex", "entry.tex"]
+ (toList
+ ( safeRelativePathFilePath
+ . resolvedSourceRelativePath
+ . Parse.parsedModuleResolved
+ <$> Parse.parsedWorkspaceImportedBeforeImporter workspace
+ ))
+
+parsesWithoutRereading :: Assertion
+parsesWithoutRereading =
+ withTemporaryDirectory "felix-source-no-reread" \temp -> do
+ let shared = temp Posix.</> "shared.tex"
+ entry = temp Posix.</> "entry.tex"
+ writeTheory shared [] "shared"
+ writeTheory entry ["shared.tex"] "entry"
+ graph <- buildSearchedGraph temp "entry.tex"
+ Directory.removeFile entry
+ Directory.removeFile shared
+ firstWorkspace <- expectRight =<< Parse.parseResolvedSourceGraph graph
+ secondWorkspace <- expectRight =<< Parse.parseResolvedSourceGraph graph
+ assertEqual "first flat projection" 2
+ (length (Parse.importedBeforeImporterBlocks firstWorkspace))
+ assertEqual "repeated downstream projection" 2
+ (length (Parse.importedBeforeImporterBlocks secondWorkspace))
+
+returnsSourceParseFailures :: Assertion
+returnsSourceParseFailures =
+ withTemporaryDirectory "felix-source-parse-error" \temp -> do
+ writeFile
+ (temp Posix.</> "entry.tex")
+ (theoryBlock "accepted"
+ <> "\\begin{axiom}\\label{late_failure}\n")
+ graph <- buildSearchedGraph temp "entry.tex"
+ emittedRef <- newIORef []
+ result <-
+ Parse.parseResolvedSourceGraphWith graph
+ (\_source block ->
+ case block of
+ Raw.BlockAxiom _location _title marker _axiom ->
+ modifyIORef' emittedRef (marker :)
+ _ ->
+ assertFailure
+ ("unexpected emitted block: " <> show block))
+ case result of
+ Left (Parse.SourceParseError source _err) -> do
+ assertEqual "failed source" "entry.tex"
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath source))
+ emitted <- reverse <$> readIORef emittedRef
+ assertEqual "completed callbacks before later failure"
+ ["accepted"]
+ emitted
+ Left err ->
+ assertFailure ("expected SourceParseError, got " <> show err)
+ Right workspace ->
+ assertFailure ("expected parse failure, got " <> show workspace)
+
+rejectsGuardedSymbolicDeclarations :: Assertion
+rejectsGuardedSymbolicDeclarations =
+ for_ [("definition", 3 :: Int), ("abbreviation", 2)]
+ \(kind, failureLine) ->
+ withTemporaryDirectory
+ ("felix-guarded-symbolic-" <> kind)
+ \temp -> do
+ let relative = "entry.tex"
+ source = unlines
+ [ "\\begin{" <> kind <> "}\\label{guarded_symbolic}"
+ , " Suppose $\\top$."
+ , " $\\guardedsymbolic{X} = X$."
+ , "\\end{" <> kind <> "}"
+ ]
+ writeFile (temp Posix.</> relative) source
+ graph <- buildSearchedGraph temp relative
+ emittedRef <- newIORef ([] :: [Raw.Block])
+ result <-
+ Parse.parseResolvedSourceGraphWith graph
+ (\_source block -> modifyIORef' emittedRef (block :))
+ case result of
+ Left (Parse.SourceParseError failed parseFailure) -> do
+ assertEqual (kind <> " source") relative
+ (safeRelativePathFilePath
+ (resolvedSourceRelativePath failed))
+ assertBool
+ (kind <> " parse failure retains a located source position: "
+ <> show parseFailure)
+ (("entry.tex " <> show failureLine <> ":")
+ `List.isInfixOf` show parseFailure)
+ assertEqual
+ (kind <> " publishes no completed source block")
+ []
+ =<< readIORef emittedRef
+ Left failure ->
+ assertFailure
+ ("expected guarded-symbolic parse failure, got "
+ <> show failure)
+ Right workspace ->
+ assertFailure
+ ("guarded symbolic " <> kind
+ <> " was silently accepted: " <> show workspace)
+
+buildSearchedGraph :: FilePath -> FilePath -> IO ResolvedSourceGraph
+buildSearchedGraph root path = do
+ mounts <- oneMount "project" root
+ request <- expectRight (searchedRoot path)
+ expectRight =<< buildResolvedSourceGraph mounts request
+
+sourceGraphOrderPaths :: ResolvedSourceGraph -> IO [FilePath]
+sourceGraphOrderPaths graph =
+ pure
+ [ safeRelativePathFilePath
+ (resolvedSourceRelativePath (sourceNodeResolved node))
+ | node <- toList (sourceGraphImportedBeforeImporter graph)
+ ]
+
+sourceNodeCanonicalPathForTest :: SourceNode -> CanonicalPath
+sourceNodeCanonicalPathForTest =
+ resolvedSourceCanonicalPath . sourceNodeResolved
+
+onlyAxiomLocation :: Parse.ParsedModule -> Location
+onlyAxiomLocation node =
+ case Parse.parsedModuleBlocks node of
+ [Raw.BlockAxiom location _title _marker _axiom] ->
+ location
+ blocks ->
+ error ("expected one axiom block, got " <> show blocks)
+
+syntaxFunctionDefinition
+ :: String
+ -> String
+ -> Maybe String
+ -> String
+syntaxFunctionDefinition marker command pragma =
+ unlines
+ ( [ "\\begin{abbreviation}\\label{" <> marker <> "}"
+ ]
+ <> maybe [] (\line -> [" " <> line]) pragma
+ <> [ " $x\\" <> command <> " y = x$."
+ , "\\end{abbreviation}"
+ ]
+ )
+
+axiomBlock :: String -> String -> String
+axiomBlock marker statement =
+ unlines
+ [ "\\begin{axiom}\\label{" <> marker <> "}"
+ , " $" <> statement <> "$."
+ , "\\end{axiom}"
+ ]
+
+assertExpressionFixity
+ :: Text
+ -> Raw.Associativity
+ -> Word8
+ -> [Interface.CanonicalLexicalEntry]
+ -> Assertion
+assertExpressionFixity marker associativity level entries =
+ case
+ [ fixity
+ | Interface.CanonicalExpressionFunction
+ _pattern
+ (Raw.Marker candidate)
+ fixity <-
+ entries
+ , candidate == marker
+ ] of
+ [Interface.Fixity actualAssociativity actualLevel] -> do
+ assertEqual
+ (Text.unpack marker <> " associativity")
+ associativity
+ actualAssociativity
+ assertEqual
+ (Text.unpack marker <> " level")
+ level
+ (Interface.mixfixLevelValue actualLevel)
+ actual ->
+ assertFailure
+ ("expected one fixity for "
+ <> Text.unpack marker
+ <> ", got "
+ <> show actual)
+
+assertAxiomLeftShape
+ :: String
+ -> String
+ -> Raw.Block
+ -> Assertion
+assertAxiomLeftShape description expected block =
+ case block of
+ Raw.BlockAxiom
+ _location
+ _title
+ _marker
+ (Raw.Axiom
+ _assumptions
+ (Raw.StmtFormula
+ (Raw.FormulaChain
+ (Raw.ChainBase
+ (expression :| [])
+ _sign
+ _relation
+ _right)))) ->
+ assertEqual
+ description
+ expected
+ (expressionShape expression)
+ _ ->
+ assertFailure
+ ("expected an axiom with one left expression, got "
+ <> show block)
+
+expressionShape :: Raw.Expr -> String
+expressionShape = \case
+ Raw.ExprVar (Raw.NamedVarAt _location name) ->
+ Text.unpack name
+ Raw.ExprOp
+ _location
+ symbol
+ arguments ->
+ let Raw.Marker marker =
+ Raw.mixfixMarker symbol
+ in
+ Text.unpack marker
+ <> "("
+ <> List.intercalate ","
+ (expressionShape <$> arguments)
+ <> ")"
+ expression ->
+ show expression
+
+findParsedModule
+ :: FilePath
+ -> Parse.ParsedSourceWorkspace
+ -> IO Parse.ParsedModule
+findParsedModule relative workspace =
+ case List.find hasPath
+ (Parse.parsedWorkspaceModules workspace) of
+ Just parsed ->
+ pure parsed
+ Nothing ->
+ assertFailure
+ ("could not find parsed module " <> relative)
+ where
+ hasPath parsed =
+ safeRelativePathFilePath
+ (resolvedSourceRelativePath
+ (Parse.parsedModuleResolved parsed))
+ == relative
+
+writeAdjectiveDefinition :: FilePath -> String -> IO ()
+writeAdjectiveDefinition path marker =
+ writeFile path (adjectiveDefinition marker)
+
+adjectiveDefinition :: String -> String
+adjectiveDefinition marker =
+ unlines
+ [ "\\begin{definition}\\label{" <> marker <> "}"
+ , " $x$ is special iff $x = x$."
+ , "\\end{definition}"
+ ]
+
+writeNounDefinition :: FilePath -> String -> IO ()
+writeNounDefinition path marker =
+ writeFile path
+ (unlines
+ [ "\\begin{definition}\\label{" <> marker <> "}"
+ , " $x$ is a special iff $x = x$."
+ , "\\end{definition}"
+ ])
+
+writeBuiltinZeroDefinition :: FilePath -> String -> IO ()
+writeBuiltinZeroDefinition path marker =
+ writeFile path (builtinZeroDefinition marker)
+
+builtinZeroDefinition :: String -> String
+builtinZeroDefinition marker =
+ unlines
+ [ "\\begin{abbreviation}\\label{" <> marker <> "}"
+ , " $\\zero = \\emptyset$."
+ , "\\end{abbreviation}"
+ ]
+
+writeBuiltinCongDefinition :: FilePath -> IO ()
+writeBuiltinCongDefinition path =
+ writeFile path
+ (unlines
+ [ "\\begin{definition}\\label{source_cong}"
+ , " $\\Cong{x}{y}{z}{w}$ iff $x = x$."
+ , "\\end{definition}"
+ ])
+
+writeTheory :: FilePath -> [FilePath] -> String -> IO ()
+writeTheory path imports label =
+ writeFile path
+ (unlines
+ (["\\import{" <> imported <> "}" | imported <- imports]
+ <> [theoryBlock label]))
+
+theoryBlock :: String -> String
+theoryBlock label =
+ unlines
+ [ "\\begin{axiom}\\label{" <> label <> "}"
+ , " $x = x$."
+ , "\\end{axiom}"
+ ]
+
+oneMount :: Text -> FilePath -> IO SourceMounts
+oneMount ident root =
+ expectRight =<< prepareSourceMounts [(sourceMountId ident, root)]
+
+withTemporaryDirectory :: String -> (FilePath -> IO a) -> IO a
+withTemporaryDirectory template =
+ bracket create Directory.removePathForcibly
+ where
+ create = do
+ systemTemp <- Directory.getTemporaryDirectory
+ (path, handle) <- openTempFile systemTemp template
+ hClose handle
+ Directory.removeFile path
+ Directory.createDirectory path
+ pure path
+
+assertRight :: (Show e, HasCallStack) => Either e a -> Assertion
+assertRight = void . expectRight
+
+expectRight :: (Show e, HasCallStack) => Either e a -> IO a
+expectRight = \case
+ Left err ->
+ assertFailure ("expected Right, got Left " <> show err)
+ Right value ->
+ pure value
+
+expectJust :: HasCallStack => String -> Maybe a -> IO a
+expectJust description = \case
+ Nothing ->
+ assertFailure ("expected " <> description)
+ Just value ->
+ pure value
+
+expectLexiconCollision
+ :: Either Parse.ParseWorkspaceError a
+ -> IO Parse.LexiconCollision
+expectLexiconCollision = \case
+ Left (Parse.SourceLexiconCollision collision) ->
+ pure collision
+ Left err ->
+ assertFailure
+ ("expected SourceLexiconCollision, got " <> show err)
+ Right _value ->
+ assertFailure "expected SourceLexiconCollision, got Right"
+
+expectTwoCollisionLocations
+ :: Parse.LexiconCollision
+ -> IO (Location, Location)
+expectTwoCollisionLocations collision =
+ case Parse.lexiconCollisionDeclarations collision of
+ firstLocation : secondLocation : _ ->
+ pure (firstLocation, secondLocation)
+ locations ->
+ assertFailure
+ ("expected two source collision locations, got "
+ <> show locations)
+
+assertLocation :: String -> FilePath -> Int -> Location -> Assertion
+assertLocation description expectedFile expectedLine location = do
+ assertEqual (description <> " file")
+ expectedFile
+ (locFile location)
+ assertEqual (description <> " line")
+ expectedLine
+ (locLine location)
+ assertEqual (description <> " column")
+ 1
+ (locColumn location)
+
+substringIndex :: String -> String -> Int
+substringIndex needle haystack =
+ fromMaybe maxBound
+ (List.findIndex
+ (List.isPrefixOf needle)
+ (List.tails haystack))
+
+assertLeft :: (Eq e, Eq a, Show e, Show a, HasCallStack) => e -> Either e a -> Assertion
+assertLeft expected actual =
+ assertEqual "expected Left value" (Left expected) actual