packages feed

hi-file-parser 0.1.8.0 → 0.1.9.0

raw patch · 19 files changed

+185/−18 lines, 19 filesbinary-addedPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

ChangeLog.md view
@@ -6,6 +6,11 @@ and this project adheres to the
 [Haskell Package Versioning Policy](https://pvp.haskell.org/).
 
+## 0.1.9.0 - 2026-06-28
+
+* Add support for GHC's support for module signatures (Backpack) from GHC 8.2.1.
+  [#33](https://github.com/commercialhaskell/hi-file-parser/pull/33)
+
 ## 0.1.8.0 - 2025-09-03
 
 * Add support for GHC 9.14 (GHC 9.14.1 onward). See
hi-file-parser.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12
 --- This file has been generated from package.yaml by hpack version 0.38.2.+-- This file has been generated from package.yaml by hpack version 0.39.6. -- -- see: https://github.com/sol/hpack  name:           hi-file-parser-version:        0.1.8.0+version:        0.1.9.0 synopsis:       Parser for GHC's *.hi files description:    A parser for GHC's *.hi files. Extracts information about dependencies and                 usage of files.@@ -15,7 +15,7 @@ author:         Hussein Ait-Lahcen <hussein.aitlahcen@gmail.com> maintainer:     Michael Snoyman <michael@snoyman.com>,                 Mike Pilgrem <public@pilgrem.com>-copyright:      Copyright 2015-2025, Stack contributors+copyright:      Copyright 2015-2026, Stack contributors license:        BSD3 license-file:   LICENSE build-type:     Simple@@ -48,12 +48,15 @@     test-files/iface/x64/ghc9067/X.hi     test-files/iface/x64/ghc9084/Main.hi     test-files/iface/x64/ghc9084/X.hi-    test-files/iface/x64/ghc9102/Main.hi-    test-files/iface/x64/ghc9102/X.hi-    test-files/iface/x64/ghc9122/Main.hi-    test-files/iface/x64/ghc9122/X.hi-    test-files/iface/x64/ghc9140/Main.hi-    test-files/iface/x64/ghc9140/X.hi+    test-files/iface/x64/ghc9103/Main.hi+    test-files/iface/x64/ghc9103/X.hi+    test-files/iface/x64/ghc9103/signatures/Consumer.hi+    test-files/iface/x64/ghc9103/signatures/LogHelper.hi+    test-files/iface/x64/ghc9103/signatures/Main.hi+    test-files/iface/x64/ghc9124/Main.hi+    test-files/iface/x64/ghc9124/X.hi+    test-files/iface/x64/ghc9141/Main.hi+    test-files/iface/x64/ghc9141/X.hi  source-repository head   type: git
src/HiFileParser.hs view
@@ -600,6 +600,9 @@       1 -> do
         skipFastString
         skipList (getTuple getFastString getModule)
+      2 -> pure ()  -- HoleUnit: no payload (see Binary Unit in
+                   -- compiler/GHC/Unit/Types.hs in GHC, e.g. line 383 on the
+                   -- ghc-9.10 branch: @put_ bh HoleUnit = putByte bh 2@).
       _ -> fail $ "Invalid unit type: " <> show idType
     Module <$> getFastString
 
@@ -638,7 +641,7 @@               all_pkgs_trust = List (zip all_pkgs (map trusted all_pkgs))
   
           -- these are new
-          skipList getModule -- sig_mods
+          skipList skipFastString -- sig_mods :: [ModuleName]
           skipList $ do -- boot_mods
             skipFastString
             getTuple skipFastString skipBool
− test-files/iface/x64/ghc9102/Main.hi

binary file changed (2861 → absent bytes)

− test-files/iface/x64/ghc9102/X.hi

binary file changed (721 → absent bytes)

+ test-files/iface/x64/ghc9103/Main.hi view

binary file changed (absent → 2863 bytes)

+ test-files/iface/x64/ghc9103/X.hi view

binary file changed (absent → 721 bytes)

+ test-files/iface/x64/ghc9103/signatures/Consumer.hi view

binary file changed (absent → 1331 bytes)

+ test-files/iface/x64/ghc9103/signatures/LogHelper.hi view

binary file changed (absent → 1013 bytes)

+ test-files/iface/x64/ghc9103/signatures/Main.hi view

binary file changed (absent → 1949 bytes)

− test-files/iface/x64/ghc9122/Main.hi

binary file changed (2086 → absent bytes)

− test-files/iface/x64/ghc9122/X.hi

binary file changed (759 → absent bytes)

+ test-files/iface/x64/ghc9124/Main.hi view

binary file changed (absent → 2084 bytes)

+ test-files/iface/x64/ghc9124/X.hi view

binary file changed (absent → 760 bytes)

− test-files/iface/x64/ghc9140/Main.hi

binary file changed (1955 → absent bytes)

− test-files/iface/x64/ghc9140/X.hi

binary file changed (879 → absent bytes)

+ test-files/iface/x64/ghc9141/Main.hi view

binary file changed (absent → 1914 bytes)

+ test-files/iface/x64/ghc9141/X.hi view

binary file changed (absent → 872 bytes)

test/HiFileParserSpec.hs view
@@ -3,9 +3,10 @@ 
 module HiFileParserSpec (spec) where
 
-import qualified HiFileParser          as Iface
+import qualified Data.List as List
+import qualified HiFileParser as Iface
 import           RIO
-import           Test.Hspec            (Spec, describe, it, shouldBe)
+import           Test.Hspec (Arg, Spec, SpecWith, describe, it, shouldBe)
 
 type Version = String
 type Directory = FilePath
@@ -35,16 +36,150 @@   , "ghc9048"  -- Last in GHC 9.4 series, using GHC 9.4.5 format
   , "ghc9067"  -- Last in GHC 9.6 series, using GHC 9.4.5 format
   , "ghc9084"  -- Last in GHC 9.8 series, using GHC 9.8.1 format
-  , "ghc9102"  -- Last in GHC 9.10 series, using GHC 9.8.1 format
-  , "ghc9122"  -- Last in GHC 9.12 series, using GHC 9.12 format
-  , "ghc9140"  -- First in GHC 9.14 series, using GHC 9.14 format
+  , "ghc9103"  -- Last in GHC 9.10 series, using GHC 9.8.1 format
+  , "ghc9124"  -- Last in GHC 9.12 series, using GHC 9.12 format
+  , "ghc9141"  -- First in GHC 9.14 series, using GHC 9.14 format
   ]
 
+-- | GHC x.y.z is represented as \"ghcxyyz\" where yy is padded with zeros.
+signatureVersions64 :: [Version]
+signatureVersions64 =
+  [ "ghc8044"  -- Last in GHC 8.4 series, using GHC 8.4.1 format
+  , "ghc8065"  -- Last in GHC 8.6 series, using GHC 8.6.1 format
+  , "ghc8084"  -- Last in GHC 8.8 series, using GHC 8.6.1 format
+  , "ghc9002"  -- Last in GHC 9.0 series, using GHC 9.0.1 format
+  , "ghc9028"  -- Last in GHC 9.2 series, using GHC 9.0.1 format
+  , "ghc9044"  -- Last using GHC 9.4.1 format
+  , "ghc9048"  -- Last in GHC 9.4 series, using GHC 9.4.5 format
+  , "ghc9067"  -- Last in GHC 9.6 series, using GHC 9.4.5 format
+  , "ghc9084"  -- Last in GHC 9.8 series, using GHC 9.8.1 format
+  , "ghc8107"  -- Last in GHC 8.10 series, using GHC 8.10.1 format
+  , "ghc9103"  -- Last in GHC 9.10 series, using GHC 9.8.1 format
+  , "ghc9124"  -- Last in GHC 9.12 series, using GHC 9.12 format
+  , "ghc9141"  -- First in GHC 9.14 series, using GHC 9.14 format
+  ]
+
+-- | Interface files that exercise two distinct bugs in
+-- @getInterfaceRecent@:
+--
+-- (A) @getModule@ recognises only unit tags @0@ (@RealUnit@) and @1@
+--     (@VirtUnit@). GHC's @Binary Unit@ instance also writes byte @2@
+--     for @HoleUnit@ (the sentinel for an un-filled signature hole).
+--     @LogHelper.hi@'s own module reference is a @VirtUnit@ whose
+--     instantiation-substitution table maps each hole name to a @Module@
+--     with @HoleUnit@ as its unit, so the parser bails at byte @2@
+--     before it ever reaches the dependency section.
+--
+-- (B) The parser reads @dep_sig_mods@ as @skipList getModule@, but GHC
+--     declares the field as @dep_sig_mods :: ![ModuleName]@ and
+--     serialises it as a list of cached @FastString@s (one byte per
+--     element on the wire in the small-index case). For an empty
+--     @dep_sig_mods@ this happens to work; for any non-empty value the
+--     parser walks off into the wrong part of the byte stream and trips
+--     on whatever byte happens to land at the next \"unit tag\" read.
+--     @Consumer.hi@ and @Main.hi@ have non-empty @dep_sig_mods@ and
+--     both trip on byte @4@.
+--
+-- Provenance: these fixtures were produced by @stack build@ in
+-- commercialhaskell/stack PR #6865 (GHC 9.10.3). That PR's cross-package
+-- support work is the most common way to make GHC write the byte
+-- patterns above in practice, but neither parser bug is specific to it.
+signatureFiles :: [FilePath]
+signatureFiles =
+  [ "LogHelper.hi"  -- indefinite library: HoleUnit reference (tag 2)
+  , "Consumer.hi"   -- instantiated library (tag 4)
+  , "Main.hi"       -- instantiated executable Main (tag 4)
+  ]
+
 spec :: Spec
-spec = describe "should successfully deserialize interface for" $ do
-   traverse_ (deserialize check32 . ("x32/" <>)) versions32
-   traverse_ (deserialize check64 . ("x64/" <>)) versions64
+spec = do
+  describe "should successfully deserialize interface for" $ do
+    traverse_ (deserialize check32 . ("x32/" <>)) versions32
+    traverse_ (deserialize check64 . ("x64/" <>)) versions64
+  describe "signature-bearing interfaces" $ do
+    traverse_ bugsAandB signatureVersions64
 
+bugsAandB :: Version -> SpecWith (Arg (IO ()))
+bugsAandB version = do
+  let stub = "x64/" <> version <> "/signatures"
+      path1 iFaceFile = stub <> "/" <> iFaceFile
+      path2 fp = "test-files/iface/" <> fp
+      logHelperPath1 = path1 "LogHelper.hi"
+      logHelperPath2 = path2 logHelperPath1
+      consumerPath1 = path1 "Consumer.hi"
+      consumerPath2 = path2 consumerPath1
+      mainPath1 = path1 "Main.hi"
+      mainPath2 = path2 mainPath1
+
+  describe "Bug A: getModule should recognise HoleUnit (tag 2)" $
+    -- Fails today with @Left "Invalid unit type: 2"@. Becomes green as
+    -- soon as @getModule@ handles tag @2@ as @HoleUnit@ with no payload,
+    -- independently of Bug B. After Bug A is fixed the file still fails
+    -- to parse fully because of Bug B (different error: \"Invalid unit
+    -- type: 6\"), but this targeted assertion is already satisfied.
+    --
+    -- ---- GHC source references ----
+    -- Mechanism: @HoleUnit@ constructor with @putByte bh 2@ in
+    -- @instance Binary Unit@. Introduced by Sylvain Henry on 2020-04-03
+    -- in commit @10d15f1ec4ba@ ("Refactoring unit management code";
+    -- commit message: "Replace BackPack fake 'hole' UnitId by a proper
+    -- HoleUnit constructor."):
+    --   https://github.com/ghc/ghc/commit/10d15f1ec4bab4dd6152d87fc66e61658a705eb3
+    --
+    -- First GHC release containing it: GHC 9.0.1, tag
+    -- @ghc-9.0.1-release@ (tag object @da53a348@, tagged 2021-02-04).
+    -- At that release the file had been renamed to
+    -- @compiler/GHC/Unit/Types.hs@; the relevant lines are:
+    --   * @| HoleUnit@ constructor, line 247:
+    --     https://github.com/ghc/ghc/blob/da53a348150d30193a6f28e1b7ddcabdf45ab726/compiler/GHC/Unit/Types.hs#L247
+    --   * @put_ bh HoleUnit = do; putByte bh 2@, lines 382-383:
+    --     https://github.com/ghc/ghc/blob/da53a348150d30193a6f28e1b7ddcabdf45ab726/compiler/GHC/Unit/Types.hs#L382-L383
+    --
+    -- Same byte-2 encoding has been carried forward unchanged in 9.2,
+    -- 9.4, 9.6, 9.8, 9.10, 9.12, 9.14 - so this bug applies to every
+    -- GHC release from 9.0.1 onward.
+    it (logHelperPath1 <> ": parser must not bail at the HoleUnit byte") $
+      parseShouldNotBailWith "Invalid unit type: 2" $ logHelperPath2
+  describe "Bug B: dep_sig_mods is [ModuleName], not [Module]" $ do
+    -- These two fixtures fail today with @Left "Invalid unit type: 4"@.
+    -- They become green as soon as @dep_sig_mods@ is read as a list of
+    -- cached FastStrings, independently of Bug A. (Their own module
+    -- references are @RealUnit@, so they never touch the HoleUnit path
+    -- - Bug A's fix is neither necessary nor sufficient.)
+    --
+    -- ---- GHC source references ----
+    -- Mechanism: @dep_sig_mods :: ![ModuleName]@ field on
+    -- @Dependencies@, written by @instance Binary Dependencies@.
+    -- Introduced by Matthew Pickering on 2021-05-05 in commit
+    -- @38faeea1a940@ ("Remove transitive information about modules and
+    -- packages from interface files", GHC issue #16885):
+    --   https://github.com/ghc/ghc/commit/38faeea1a94072ffd9f459d9fe570f06bc1da84a
+    --
+    -- The GHC 9.2 release branch was forked before this commit landed
+    -- (merge-base of the commit and the @ghc-9.2.1-release@ tag is
+    -- @05c5c054@, 2021-03-15), so GHC 9.2.x shipped without the field.
+    -- First GHC release containing it: GHC 9.4.1, tag
+    -- @ghc-9.4.1-release@ (tag object @c24e9dc5@, tagged 2022-08-07).
+    -- At that release, in @compiler/GHC/Unit/Module/Deps.hs@:
+    --   * @, dep_sig_mods :: ![ModuleName]@, line 62:
+    --     https://github.com/ghc/ghc/blob/6d01245c458c49ca25c89ec13be3268ab6930a27/compiler/GHC/Unit/Module/Deps.hs#L62
+    --   * @put_ bh (dep_sig_mods deps)@ (inside the @instance Binary
+    --     Dependencies@ block at lines 111-119), line 115:
+    --     https://github.com/ghc/ghc/blob/6d01245c458c49ca25c89ec13be3268ab6930a27/compiler/GHC/Unit/Module/Deps.hs#L115
+    --
+    -- The field has been carried forward unchanged in 9.6, 9.8, 9.10,
+    -- 9.12, 9.14 - so this bug applies to every GHC release from 9.4.1
+    -- onward.
+    it (consumerPath1 <> ": parser must not misalign on dep_sig_mods") $
+      parseShouldNotBailWith "Invalid unit type: 4" $ consumerPath2
+    it (mainPath1 <> ": parser must not misalign on dep_sig_mods") $
+      parseShouldNotBailWith "Invalid unit type: 4" $ mainPath2
+  describe "Bug A + Bug B: should fully deserialize" $
+    -- Stricter than the per-bug assertions above: requires both fixes
+    -- to be in place. Useful as the regression gate once the bugs are
+    -- closed.
+    traverse_ (deserializeFile stub) signatureFiles
+
 check32 :: Iface.Interface -> IO ()
 check32 iface = do
     hasExpectedUsage "some-dependency.txt" iface `shouldBe` True
@@ -63,6 +198,27 @@         case result of
           (Left msg)    -> fail msg
           (Right iface) -> check iface
+
+deserializeFile :: Directory -> FilePath -> Spec
+deserializeFile d fname =
+    it (d <> "/" <> fname) $ do
+        let ifacePath = "test-files/iface/" <> d <> "/" <> fname
+        result <- Iface.fromFile ifacePath
+        case result of
+          Left msg -> fail msg
+          Right _  -> pure ()
+
+-- | Assert that @Iface.fromFile@ either succeeds or fails with a message that
+-- does not contain the given substring. Lets a per-bug failing test pass as
+-- soon as that bug alone is fixed, even if other unrelated bugs leave the
+-- parser still erroring on the same fixture with a different message.
+parseShouldNotBailWith :: String -> FilePath -> IO ()
+parseShouldNotBailWith forbidden fp = do
+    result <- Iface.fromFile fp
+    case result of
+      Left msg | forbidden `List.isInfixOf` msg ->
+        fail $ "parser bailed with " <> show forbidden <> ": " <> msg
+      _ -> pure ()
 
 -- | `Usage` is the name given by GHC to TH dependency
 hasExpectedUsage :: Usage -> Iface.Interface -> Bool