From 72edd0290b376fb1ded6b722f7af5eaac9c383d8 Mon Sep 17 00:00:00 2001 From: Dominic Orchard Date: Fri, 9 Jan 2026 17:56:21 +0000 Subject: [PATCH 1/9] store only the base name in a modfile --- src/Language/Fortran/Util/ModFile.hs | 4 +++- test/Language/Fortran/Analysis/ModFileSpec.hs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Language/Fortran/Util/ModFile.hs b/src/Language/Fortran/Util/ModFile.hs index 9079a180..b4d2c5fa 100644 --- a/src/Language/Fortran/Util/ModFile.hs +++ b/src/Language/Fortran/Util/ModFile.hs @@ -147,7 +147,9 @@ regenModFile pf mf = mf { mfModuleMap = extractModuleMap pf , mfDeclMap = extractDeclMap pf , mfTypeEnv = FAT.extractTypeEnvExtended pf , mfParamVarMap = extractParamVarMap pf - , mfFilename = F.pfGetFilename pf } + -- Store only the basename for portability; the .fsmod + -- file's location provides the directory context + , mfFilename = System.FilePath.takeFileName (F.pfGetFilename pf) } -- | Generate a fresh ModFile from the module map, declaration map and -- type analysis of a given analysed and renamed ProgramFile. diff --git a/test/Language/Fortran/Analysis/ModFileSpec.hs b/test/Language/Fortran/Analysis/ModFileSpec.hs index 9ea4a245..f5ecfc65 100644 --- a/test/Language/Fortran/Analysis/ModFileSpec.hs +++ b/test/Language/Fortran/Analysis/ModFileSpec.hs @@ -39,8 +39,8 @@ testModuleMaps = do -- parse all files into mod files pfs <- mapM (\p -> pParser p) paths let modFiles = map genModFile pfs - -- get unique name to filemap - let mmap = genUniqNameToFilenameMap "" modFiles + -- get unique name to filemap (pass the directory as localPath) + let mmap = genUniqNameToFilenameMap fixturePath modFiles -- check that `constant` is declared in leaf.f90 let Just (leaf, _) = M.lookup "leaf_constant_1" mmap leaf `shouldBe` ("test-data" "module" "leaf.f90") From f119ffb5f26ff293c5974f40e508c2cdf52f096c Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Thu, 22 Jan 2026 12:02:07 +0000 Subject: [PATCH 2/9] Check for existence of source files referenced in modfiles Show a warning if we cannot find the original source file. This should help prevent some silent failures. --- src/Language/Fortran/Util/ModFile.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Language/Fortran/Util/ModFile.hs b/src/Language/Fortran/Util/ModFile.hs index b4d2c5fa..9ab9df0c 100644 --- a/src/Language/Fortran/Util/ModFile.hs +++ b/src/Language/Fortran/Util/ModFile.hs @@ -210,6 +210,12 @@ decodeModFiles = foldM (\ modFiles d -> do return [(modFileName, emptyModFile)] Right mods -> do hPutStrLn stderr $ modFileName ++ ": successfully parsed precompiled file." + -- Check if the source files referenced in mfFilename exist + forM_ mods $ \ mf -> do + let srcFile = d mfFilename mf + exists <- doesFileExist srcFile + unless exists $ + hPutStrLn stderr $ modFileName ++ ": Warning: source file not found: " ++ srcFile return $ map (modFileName,) mods return $ addedModFiles ++ modFiles ) [] -- can't use emptyModFiles From 5047f2224026e9cff29c704e907b0b192c6ba3a5 Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Wed, 28 Jan 2026 08:54:53 +0000 Subject: [PATCH 3/9] Add CI for GHC 9.6 - 9.12 on Ubuntu --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dacb8044..f46f1362 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,10 @@ jobs: - "9.0" - "9.2" - "9.4" + - "9.6" + - "9.8" + - "9.10" + - "9.12" steps: From f42e354fd0bcacb9729d032598c8552cf6f867b9 Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Mon, 2 Mar 2026 23:46:38 +0000 Subject: [PATCH 4/9] Add XXH3-64 source hash to ModFile format for reliable validation Replaces timestamp-based validation with content hash to detect source file changes reliably. ModFile format breaking change, version bump to 0.17.0. --- app/Main.hs | 7 +- fortran-src.cabal | 11 +- package.yaml | 3 +- src/Language/Fortran/Util/ModFile.hs | 133 +++++++++++++++--- test/Language/Fortran/Analysis/ModFileSpec.hs | 5 +- 5 files changed, 131 insertions(+), 28 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 4bc50b93..b8916c7b 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -82,8 +82,8 @@ main = do , not (null nxt) = do let fnPaths = [ fn | (_, Just (MOFile fn)) <- nxt ] newMods <- fmap concat . forM fnPaths $ \ fnPath -> do - tsStatus <- checkTimestamps fnPath - case tsStatus of + hashStatus <- checkModFileHash fnPath + case hashStatus of NoSuchFile -> do putStr $ "Does not exist: " ++ fnPath pure [emptyModFile] @@ -221,7 +221,8 @@ compileFileToMod mvers mods path moutfile = do let version = fromMaybe (deduceFortranVersion path) mvers mmap = combinedModuleMap mods tenv = stripExtended $ combinedTypeEnv mods - runCompile = genModFile . fst . analyseTypesWithEnv tenv . analyseRenamesWithModuleMap mmap . initAnalysis + sourceHash = computeSourceHash contents + runCompile = genModFile sourceHash . fst . analyseTypesWithEnv tenv . analyseRenamesWithModuleMap mmap . initAnalysis parsedPF <- case (Parser.byVerWithMods mods version) path contents of Right pf -> return pf diff --git a/fortran-src.cabal b/fortran-src.cabal index 41d429c6..83fb1226 100644 --- a/fortran-src.cabal +++ b/fortran-src.cabal @@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.39.1. +-- This file has been generated from package.yaml by hpack version 0.38.1. -- -- see: https://github.com/sol/hpack name: fortran-src -version: 0.16.9 +version: 0.17.0 synopsis: Parsers and analyses for Fortran standards 66, 77, 90, 95 and 2003 (partial). description: Provides lexing, parsing, and basic analyses of Fortran code covering standards: FORTRAN 66, FORTRAN 77, Fortran 90, Fortran 95, Fortran 2003 (partial) and some legacy extensions. Includes data flow and basic block analysis, a renamer, and type analysis. For example usage, see the @@ project, which uses fortran-src as its front end. category: Language @@ -27,9 +27,13 @@ extra-source-files: test-data/f77-include/foo.f test-data/f77-include/no-newline/foo.f test-data/module/leaf.f90 + test-data/module/leaf.fsmod test-data/module/mid1.f90 + test-data/module/mid1.fsmod test-data/module/mid2.f90 + test-data/module/mid2.fsmod test-data/module/top.f90 + test-data/module/top.fsmod test-data/rewriter/replacementsmap-columnlimit/001_foo.f test-data/rewriter/replacementsmap-columnlimit/001_foo.f.expected test-data/rewriter/replacementsmap-columnlimit/002_other.f @@ -205,6 +209,7 @@ library , temporary >=1.2 && <1.4 , text >=1.2 && <2.2 , uniplate >=1.6 && <2 + , xxhash-ffi ==0.3.* default-language: Haskell2010 if os(windows) cpp-options: -DFS_DISABLE_WIN_BROKEN_TESTS @@ -269,6 +274,7 @@ executable fortran-src , temporary >=1.2 && <1.4 , text >=1.2 && <2.2 , uniplate >=1.6 && <2 + , xxhash-ffi ==0.3.* default-language: Haskell2010 if os(windows) cpp-options: -DFS_DISABLE_WIN_BROKEN_TESTS @@ -368,6 +374,7 @@ test-suite spec , temporary >=1.2 && <1.4 , text >=1.2 && <2.2 , uniplate >=1.6 && <2 + , xxhash-ffi ==0.3.* default-language: Haskell2010 if os(windows) cpp-options: -DFS_DISABLE_WIN_BROKEN_TESTS diff --git a/package.yaml b/package.yaml index 0f6a75fa..25a3f78d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: fortran-src -version: '0.16.9' +version: '0.17.0' synopsis: Parsers and analyses for Fortran standards 66, 77, 90, 95 and 2003 (partial). description: >- Provides lexing, parsing, and basic analyses of Fortran code covering @@ -98,6 +98,7 @@ dependencies: - temporary >=1.2 && <1.4 - either ^>=5.0.1.1 - process >= 1.2.0.0 +- xxhash-ffi >= 0.3 && < 0.4 - singletons >= 3.0 && < 3.6 diff --git a/src/Language/Fortran/Util/ModFile.hs b/src/Language/Fortran/Util/ModFile.hs index 9ab9df0c..099cfded 100644 --- a/src/Language/Fortran/Util/ModFile.hs +++ b/src/Language/Fortran/Util/ModFile.hs @@ -23,9 +23,16 @@ renamer. The other data is up to you. Note that the encoder and decoder work on lists of ModFile so that one fsmod-file may contain information about multiple Fortran files. +Each ModFile includes a source hash (XXH3-64, 8 bytes) to verify that the +precompiled information matches the current source file. Use +'checkSourceHash' to validate before using a ModFile. + One typical usage might look like: -> let modFile1 = genModFile programFile +> contents <- flexReadFile path +> let sourceHash = computeSourceHash contents +> -- ... parse contents into programFile ... +> let modFile1 = genModFile sourceHash programFile > let modFile2 = alterModFileData (const (Just ...)) "mydata" modFile1 > let bytes = encodeModFile [modFile2] > ... @@ -45,6 +52,9 @@ module Language.Fortran.Util.ModFile ModFile, ModFiles, emptyModFile, emptyModFiles, modFileSuffix , lookupModFileData, getLabelsModFileData, alterModFileData, alterModFileDataF + -- * Source hashing + , SourceHash, computeSourceHash, computeFileHash + -- * Creation , genModFile, regenModFile @@ -58,7 +68,7 @@ module Language.Fortran.Util.ModFile , extractModuleMap, combinedModuleMap, localisedModuleMap, combinedTypeEnv , ParamVarMap, extractParamVarMap, combinedParamVarMap , genUniqNameToFilenameMap - , TimestampStatus(..), checkTimestamps + , HashStatus(..), checkModFileHash, checkSourceHash, checkTimestamps ) where import qualified Language.Fortran.AST as F @@ -73,19 +83,30 @@ import Language.Fortran.Util.Files ( getDirContents ) import Control.Monad.State import Control.Monad -- required for mtl-2.3 (GHC 9.6) import Data.Binary (Binary, encode, decodeOrFail) +import Data.Bits (shiftR, (.&.)) +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as LB +import qualified Data.ByteString.Unsafe as BU import Data.Data +import qualified Data.Digest.XXHash.FFI.C as XXH import Data.Generics.Uniplate.Operations import qualified Data.Map.Strict as M import Data.Maybe +import Data.Word (Word8, Word64) +import Foreign.C.Types (CSize(..), CULLong(..)) import GHC.Generics (Generic) import System.Directory ( doesFileExist, getModificationTime ) import qualified System.FilePath import System.FilePath ( (-<.>), (), normalise ) import System.IO ( hPutStrLn, stderr ) +import System.IO.Unsafe ( unsafePerformIO ) -------------------------------------------------- +-- | Type alias for source file hash (XXH3-64, 8 bytes) +type SourceHash = B.ByteString + -- | Standard ending of fortran-src-format "mod files" modFileSuffix :: String modFileSuffix = ".fsmod" @@ -117,6 +138,7 @@ type ParamVarMap = FAD.ParameterVarMap -- | The data stored in the "mod files" data ModFile = ModFile { mfFilename :: String + , mfSourceHash :: SourceHash -- ^ XXH3-64 hash of source file (8 bytes) , mfStringMap :: StringMap , mfModuleMap :: FAR.ModuleMap , mfDeclMap :: DeclMap @@ -137,24 +159,53 @@ emptyModFiles = [] -- | Starting point. emptyModFile :: ModFile -emptyModFile = ModFile "" M.empty M.empty M.empty M.empty M.empty M.empty +emptyModFile = ModFile "" B.empty M.empty M.empty M.empty M.empty M.empty M.empty + +-- | Convert Word64 to 8-byte ByteString (little-endian) +word64ToBytes :: Word64 -> B.ByteString +word64ToBytes w = B.pack + [ fromIntegral (w .&. 0xFF) + , fromIntegral ((w `shiftR` 8) .&. 0xFF) + , fromIntegral ((w `shiftR` 16) .&. 0xFF) + , fromIntegral ((w `shiftR` 24) .&. 0xFF) + , fromIntegral ((w `shiftR` 32) .&. 0xFF) + , fromIntegral ((w `shiftR` 40) .&. 0xFF) + , fromIntegral ((w `shiftR` 48) .&. 0xFF) + , fromIntegral ((w `shiftR` 56) .&. 0xFF) + ] + +-- | Compute XXH3-64 hash from file contents (strict ByteString). +-- Use this when you've already read the file. +computeSourceHash :: B.ByteString -> SourceHash +computeSourceHash contents = unsafePerformIO $ do + hash <- BU.unsafeUseAsCStringLen contents $ \(ptr, len) -> + XXH.c_xxh3_64bits_withSeed ptr (fromIntegral len) 0 + return $ word64ToBytes (fromIntegral hash) + +-- | Compute XXH3-64 hash of a file's contents. +-- Convenience function when you have a filepath and haven't read the file yet. +computeFileHash :: FilePath -> IO SourceHash +computeFileHash path = do + contents <- B.readFile path + return $ computeSourceHash contents -- | Extracts the module map, declaration map and type analysis from -- an analysed and renamed ProgramFile, then inserts it into the -- ModFile. -regenModFile :: forall a. (Data a) => F.ProgramFile (FA.Analysis a) -> ModFile -> ModFile -regenModFile pf mf = mf { mfModuleMap = extractModuleMap pf - , mfDeclMap = extractDeclMap pf - , mfTypeEnv = FAT.extractTypeEnvExtended pf - , mfParamVarMap = extractParamVarMap pf - -- Store only the basename for portability; the .fsmod - -- file's location provides the directory context - , mfFilename = System.FilePath.takeFileName (F.pfGetFilename pf) } +regenModFile :: forall a. (Data a) => SourceHash -> F.ProgramFile (FA.Analysis a) -> ModFile -> ModFile +regenModFile srcHash pf mf = mf { mfSourceHash = srcHash + , mfModuleMap = extractModuleMap pf + , mfDeclMap = extractDeclMap pf + , mfTypeEnv = FAT.extractTypeEnvExtended pf + , mfParamVarMap = extractParamVarMap pf + -- Store only the basename for portability; the .fsmod + -- file's location provides the directory context + , mfFilename = System.FilePath.takeFileName (F.pfGetFilename pf) } -- | Generate a fresh ModFile from the module map, declaration map and -- type analysis of a given analysed and renamed ProgramFile. -genModFile :: forall a. (Data a) => F.ProgramFile (FA.Analysis a) -> ModFile -genModFile = flip regenModFile emptyModFile +genModFile :: forall a. (Data a) => SourceHash -> F.ProgramFile (FA.Analysis a) -> ModFile +genModFile srcHash pf = regenModFile srcHash pf emptyModFile -- | Looks up the raw "other data" that may be stored in a ModFile by -- applications that make use of fortran-src. @@ -368,22 +419,64 @@ extractParamVarMap pf = M.fromList cvm , (F.Declarator _ _ v F.ScalarDecl _ _) <- universeBi st :: [F.Declarator (FA.Analysis a)] , Just con <- [FA.constExp (F.getAnnotation v)] ] --- | Status of mod-file compared to Fortran file. -data TimestampStatus = NoSuchFile | CompileFile | ModFileExists FilePath +-- | Status of mod-file compared to Fortran file (hash-based). +data HashStatus = NoSuchFile | CompileFile | ModFileExists FilePath + deriving (Eq, Show) + +-- | Status of mod-file compared to Fortran file (timestamp-based, deprecated). +data TimestampStatus = TSNoSuchFile | TSCompileFile | TSModFileExists FilePath + deriving (Eq, Show) + +-- | Check if a ModFile needs recompiling by comparing source file hash. +-- Checks if both source and .fsmod exist, loads the .fsmod, and compares hashes. +-- Returns whether to compile or use the existing ModFile. +checkModFileHash :: FilePath -> IO HashStatus +checkModFileHash path = do + pathExists <- doesFileExist path + let modPath = path -<.> modFileSuffix + modExists <- doesFileExist modPath + case (pathExists, modExists) of + (False, _) -> pure NoSuchFile + (True, False) -> pure CompileFile + (True, True) -> do + -- Load the modfile and check if hash matches + contents <- LB.readFile modPath + case decodeModFile contents of + Left _ -> pure CompileFile -- Corrupted modfile, recompile + Right [] -> pure CompileFile -- Empty modfile, recompile + Right (modFile:_) -> do + -- Check hash of source file + currentHash <- computeFileHash path + if currentHash == mfSourceHash modFile + then pure $ ModFileExists modPath + else pure CompileFile + +-- | Compare the source file hash to the hash stored in the ModFile. +-- This is the preferred method for checking if a ModFile is up-to-date. +checkSourceHash :: FilePath -> ModFile -> IO HashStatus +checkSourceHash path modFile = do + pathExists <- doesFileExist path + if not pathExists + then pure NoSuchFile + else do + currentHash <- computeFileHash path + if currentHash == mfSourceHash modFile + then pure $ ModFileExists (path -<.> modFileSuffix) + else pure CompileFile -- | Compare the source file timestamp to the fsmod file timestamp, if --- it exists. +-- it exists. DEPRECATED: Use checkSourceHash instead for more reliable validation. checkTimestamps :: FilePath -> IO TimestampStatus checkTimestamps path = do pathExists <- doesFileExist path modExists <- doesFileExist $ path -<.> modFileSuffix case (pathExists, modExists) of - (False, _) -> pure NoSuchFile - (True, False) -> pure CompileFile + (False, _) -> pure TSNoSuchFile + (True, False) -> pure TSCompileFile (True, True) -> do let modPath = path -<.> modFileSuffix pathModTime <- getModificationTime path modModTime <- getModificationTime modPath if pathModTime < modModTime - then pure $ ModFileExists modPath - else pure CompileFile + then pure $ TSModFileExists modPath + else pure TSCompileFile diff --git a/test/Language/Fortran/Analysis/ModFileSpec.hs b/test/Language/Fortran/Analysis/ModFileSpec.hs index f5ecfc65..6f2cf5ec 100644 --- a/test/Language/Fortran/Analysis/ModFileSpec.hs +++ b/test/Language/Fortran/Analysis/ModFileSpec.hs @@ -37,8 +37,9 @@ testModuleMaps = do let fixturePath = "test-data" "module" paths <- expandDirs [fixturePath] -- parse all files into mod files - pfs <- mapM (\p -> pParser p) paths - let modFiles = map genModFile pfs + pfs <- mapM pParser paths + hashes <- mapM computeFileHash paths + let modFiles = zipWith genModFile hashes pfs -- get unique name to filemap (pass the directory as localPath) let mmap = genUniqNameToFilenameMap fixturePath modFiles -- check that `constant` is declared in leaf.f90 From 8b6a8a0ab825b8233ba366262535b23f19656dce Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Tue, 3 Mar 2026 10:11:26 +0000 Subject: [PATCH 5/9] remove spurious fsmod files from .cabal --- fortran-src.cabal | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fortran-src.cabal b/fortran-src.cabal index 83fb1226..c2d8184b 100644 --- a/fortran-src.cabal +++ b/fortran-src.cabal @@ -27,13 +27,9 @@ extra-source-files: test-data/f77-include/foo.f test-data/f77-include/no-newline/foo.f test-data/module/leaf.f90 - test-data/module/leaf.fsmod test-data/module/mid1.f90 - test-data/module/mid1.fsmod test-data/module/mid2.f90 - test-data/module/mid2.fsmod test-data/module/top.f90 - test-data/module/top.fsmod test-data/rewriter/replacementsmap-columnlimit/001_foo.f test-data/rewriter/replacementsmap-columnlimit/001_foo.f.expected test-data/rewriter/replacementsmap-columnlimit/002_other.f From 749afcbb22f425a26becf690940d2383f3f3e3ff Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Tue, 3 Mar 2026 10:21:08 +0000 Subject: [PATCH 6/9] Display source hash in --dump-mod-file output --- app/Main.hs | 17 ++++++++++------- src/Language/Fortran/Util/ModFile.hs | 6 +++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index b8916c7b..d92860a2 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -6,6 +6,7 @@ module Main ( main ) where import Prelude hiding (readFile, mod) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Word (Word8) import Text.PrettyPrint (render) @@ -159,13 +160,15 @@ main = do case decodeModFile contents' of Left msg -> putStrLn $ "Error: " ++ msg Right mfs -> forM_ mfs $ \ mf -> - putStrLn $ "Filename: " ++ moduleFilename mf ++ - "\n\nStringMap:\n" ++ showStringMap (combinedStringMap [mf]) ++ - "\n\nModuleMap:\n" ++ showModuleMap (combinedModuleMap [mf]) ++ - "\n\nDeclMap:\n" ++ showGenericMap (combinedDeclMap [mf]) ++ - "\n\nTypeEnv:\n" ++ showTypes (combinedTypeEnv [mf]) ++ - "\n\nParamVarMap:\n" ++ showGenericMap (combinedParamVarMap [mf]) ++ - "\n\nOther Data Labels: " ++ show (getLabelsModFileData mf) + let hashHex = concatMap (printf "%02x") (B.unpack (moduleSourceHash mf)) + in putStrLn $ "Filename: " ++ moduleFilename mf ++ + "\nSource Hash (XXH3-64): " ++ hashHex ++ + "\n\nStringMap:\n" ++ showStringMap (combinedStringMap [mf]) ++ + "\n\nModuleMap:\n" ++ showModuleMap (combinedModuleMap [mf]) ++ + "\n\nDeclMap:\n" ++ showGenericMap (combinedDeclMap [mf]) ++ + "\n\nTypeEnv:\n" ++ showTypes (combinedTypeEnv [mf]) ++ + "\n\nParamVarMap:\n" ++ showGenericMap (combinedParamVarMap [mf]) ++ + "\n\nOther Data Labels: " ++ show (getLabelsModFileData mf) ShowFlows isFrom isSuper astBlockId -> do let pf = analyseParameterVars pvm . analyseBBlocks . diff --git a/src/Language/Fortran/Util/ModFile.hs b/src/Language/Fortran/Util/ModFile.hs index 099cfded..354ac541 100644 --- a/src/Language/Fortran/Util/ModFile.hs +++ b/src/Language/Fortran/Util/ModFile.hs @@ -62,7 +62,7 @@ module Language.Fortran.Util.ModFile , encodeModFile, decodeModFile, decodeModFiles, decodeModFiles' -- * Operations - , moduleFilename + , moduleFilename, moduleSourceHash , StringMap, extractStringMap, combinedStringMap , DeclContext(..), DeclMap, extractDeclMap, combinedDeclMap , extractModuleMap, combinedModuleMap, localisedModuleMap, combinedTypeEnv @@ -308,6 +308,10 @@ combinedParamVarMap = M.unions . map mfParamVarMap moduleFilename :: ModFile -> String moduleFilename = mfFilename +-- | Get the source hash from the ModFile. +moduleSourceHash :: ModFile -> SourceHash +moduleSourceHash = mfSourceHash + -------------------------------------------------- -- | Create a map that links all unique variable/function names in the From caff9c6968816a21af42da25dfba0b6b29e09a7e Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Tue, 3 Mar 2026 12:30:25 +0000 Subject: [PATCH 7/9] Fix file locking issue in checkModFileHash with strict evaluation --- src/Language/Fortran/Util/ModFile.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Language/Fortran/Util/ModFile.hs b/src/Language/Fortran/Util/ModFile.hs index 354ac541..9667e8cb 100644 --- a/src/Language/Fortran/Util/ModFile.hs +++ b/src/Language/Fortran/Util/ModFile.hs @@ -80,6 +80,7 @@ import qualified Language.Fortran.Analysis.Types as FAT import qualified Language.Fortran.Util.Position as P import Language.Fortran.Util.Files ( getDirContents ) +import Control.Exception (evaluate) import Control.Monad.State import Control.Monad -- required for mtl-2.3 (GHC 9.6) import Data.Binary (Binary, encode, decodeOrFail) @@ -444,8 +445,11 @@ checkModFileHash path = do (True, False) -> pure CompileFile (True, True) -> do -- Load the modfile and check if hash matches - contents <- LB.readFile modPath - case decodeModFile contents of + -- Use strict read to avoid lazy I/O file locking issues + contents <- BL.readFile modPath + !strictContents <- evaluate (BL.toStrict contents) + let lazyContents = BL.fromStrict strictContents + case decodeModFile lazyContents of Left _ -> pure CompileFile -- Corrupted modfile, recompile Right [] -> pure CompileFile -- Empty modfile, recompile Right (modFile:_) -> do From 85cc3b8cb1a27892dbbff43675e31dae4e1542ba Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Tue, 3 Mar 2026 17:14:34 +0000 Subject: [PATCH 8/9] Add xxhash-ffi to GHC 9.2 and 9.4 Nix configurations --- haskell-flake-ghc92.nix | 5 +++++ haskell-flake-ghc94.nix | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/haskell-flake-ghc92.nix b/haskell-flake-ghc92.nix index 67443517..ed60b33c 100644 --- a/haskell-flake-ghc92.nix +++ b/haskell-flake-ghc92.nix @@ -12,8 +12,13 @@ pkgs: { singletons.source = "3.0.1"; # req because singletons-th-3.1 had bad bounds th-desugar.source = "1.13.1"; th-abstraction.source = "0.4.5.0"; + xxhash-ffi.source = "0.3"; }; + otherOverlays = [ + (self: super: { libxxhash = pkgs.xxHash; }) + ]; + # (note this is actually unused/we have to duplicate because it doesn't get # packed into basePackages or any key we can use... but nice to document here) devShell = { diff --git a/haskell-flake-ghc94.nix b/haskell-flake-ghc94.nix index 6ebf68e8..d561e1a7 100644 --- a/haskell-flake-ghc94.nix +++ b/haskell-flake-ghc94.nix @@ -12,8 +12,13 @@ pkgs: { #singletons.source = "3.0.1"; th-desugar.source = "1.14"; th-abstraction.source = "0.4.5.0"; + xxhash-ffi.source = "0.3"; }; + otherOverlays = [ + (self: super: { libxxhash = pkgs.xxHash; }) + ]; + # (note this is actually unused/we have to duplicate because it doesn't get # packed into basePackages or any key we can use... but nice to document here) devShell = { From 25103862f997c1d80a58dfb4586d5b5d9fcea236 Mon Sep 17 00:00:00 2001 From: Matthew Danish Date: Fri, 6 Mar 2026 13:38:52 +0100 Subject: [PATCH 9/9] Document xxHash dependency and mod file compat break in 0.17.0 --- CHANGELOG.md | 5 +++++ README.md | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e70d70..9c126edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 0.17.0 + * Add XXH3-64 source hash to mod files for reliable change detection + * **breaks mod file compatibility** with previous versions + * New C library dependency: xxHash (Haskell: `xxhash-ffi`) + ### 0.16.9 * Added support for legacy features in Fortran 90 free-form style (selected via `--fortranVersion=Fortran90Legacy`) diff --git a/README.md b/README.md index 13705e31..edabff0f 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,10 @@ the file name: * Unknown extensions are parsed like `*.f` files. ## Building -You will need the GMP library plus header files: on many platforms, this will be -via the package `libgmp-dev`. +You will need the following C libraries plus header files: + + * GMP: on many platforms, via the package `libgmp-dev` + * xxHash: on many platforms, via the package `libxxhash-dev` Haskell library dependencies are listed in `package.yaml`. fortran-src supports building with Stack or Cabal.