From d3874407df4223a5e14a43571f4cc344349a537d Mon Sep 17 00:00:00 2001
From: Torsten Schmits <git@tryp.io>
Date: Wed, 2 Aug 2023 19:35:37 +0200
Subject: [PATCH] Fix several mistakes around free variables in iface
 breakpoints

Fixes #23612 , #23607, #23998 and #23666.

MR: !11026

The fingerprinting logic in `Iface.Recomp` failed lookups when processing decls containing breakpoints for two reasons:

* IfaceBreakpoint created binders for free variables instead of expressions

* When collecting free names for the dependency analysis for fingerprinting, breakpoint FVs were skipped
---
 compiler/GHC/Core/Opt/SpecConstr.hs        | 13 +++---------
 compiler/GHC/Core/Opt/Specialise.hs        |  7 ++-----
 compiler/GHC/Core/Subst.hs                 |  2 ++
 compiler/GHC/CoreToIface.hs                |  2 +-
 compiler/GHC/Iface/Syntax.hs               |  9 +++++++--
 compiler/GHC/IfaceToCore.hs                |  4 ++--
 testsuite/tests/ghci/T23612/T23612.hs      | 23 ++++++++++++++++++++++
 testsuite/tests/ghci/T23612/T23612.script  |  1 +
 testsuite/tests/ghci/T23612/T23612b.script |  1 +
 testsuite/tests/ghci/T23612/T23612bA.hs    |  5 +++++
 testsuite/tests/ghci/T23612/T23612bB.hs    |  5 +++++
 testsuite/tests/ghci/T23612/all.T          |  2 ++
 12 files changed, 54 insertions(+), 20 deletions(-)
 create mode 100644 testsuite/tests/ghci/T23612/T23612.hs
 create mode 100644 testsuite/tests/ghci/T23612/T23612.script
 create mode 100644 testsuite/tests/ghci/T23612/T23612b.script
 create mode 100644 testsuite/tests/ghci/T23612/T23612bA.hs
 create mode 100644 testsuite/tests/ghci/T23612/T23612bB.hs
 create mode 100644 testsuite/tests/ghci/T23612/all.T

diff --git a/compiler/GHC/Core/Opt/SpecConstr.hs b/compiler/GHC/Core/Opt/SpecConstr.hs
index 1721496f03b6..09460812a435 100644
--- a/compiler/GHC/Core/Opt/SpecConstr.hs
+++ b/compiler/GHC/Core/Opt/SpecConstr.hs
@@ -1480,8 +1480,7 @@ scExpr' env (Type t)     =
 scExpr' env (Coercion c) = return (nullUsage, Coercion (scSubstCo env c))
 scExpr' _   e@(Lit {})   = return (nullUsage, e)
 scExpr' env (Tick t e)   = do (usg, e') <- scExpr env e
-                              (usg_t, t') <- scTickish env t
-                              return (combineUsage usg usg_t, Tick t' e')
+                              return (usg, Tick (scTickish env t) e')
 scExpr' env (Cast e co)  = do (usg, e') <- scExpr env e
                               return (usg, mkCast e' (scSubstCo env co))
                               -- Important to use mkCast here
@@ -1543,14 +1542,8 @@ scExpr' env (Case scrut b ty alts)
 -- | Substitute the free variables captured by a breakpoint.
 -- Variables are dropped if they have a non-variable substitution, like in
 -- 'GHC.Opt.Specialise.specTickish'.
-scTickish :: ScEnv -> CoreTickish -> UniqSM (ScUsage, CoreTickish)
-scTickish env = \case
-  Breakpoint ext i fv modl -> do
-    (usg, fv') <- unzip <$> mapM (\ v -> scExpr env (Var v)) fv
-    pure (combineUsages usg, Breakpoint ext i [v | Var v <- fv'] modl)
-  t@ProfNote {} -> pure (nullUsage, t)
-  t@HpcTick {} -> pure (nullUsage, t)
-  t@SourceNote {} -> pure (nullUsage, t)
+scTickish :: ScEnv -> CoreTickish -> CoreTickish
+scTickish SCE {sc_subst = subst} = substTickish subst
 
 {- Note [Do not specialise evals]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/compiler/GHC/Core/Opt/Specialise.hs b/compiler/GHC/Core/Opt/Specialise.hs
index f36eb67af603..29688e709eeb 100644
--- a/compiler/GHC/Core/Opt/Specialise.hs
+++ b/compiler/GHC/Core/Opt/Specialise.hs
@@ -67,6 +67,7 @@ import GHC.Core.Unfold
 
 import Data.List( partition )
 import Data.List.NonEmpty ( NonEmpty (..) )
+import GHC.Core.Subst (substTickish)
 
 {-
 ************************************************************************
@@ -1267,11 +1268,7 @@ specLam env bndrs body
 
 --------------
 specTickish :: SpecEnv -> CoreTickish -> CoreTickish
-specTickish (SE { se_subst = subst }) (Breakpoint ext ix ids modl)
-  = Breakpoint ext ix [ id' | id <- ids, Var id' <- [Core.lookupIdSubst subst id]] modl
-  -- drop vars from the list if they have a non-variable substitution.
-  -- should never happen, but it's harmless to drop them anyway.
-specTickish _ other_tickish = other_tickish
+specTickish (SE { se_subst = subst }) bp = substTickish subst bp
 
 --------------
 specCase :: SpecEnv
diff --git a/compiler/GHC/Core/Subst.hs b/compiler/GHC/Core/Subst.hs
index c84f72989c48..23c643b142a7 100644
--- a/compiler/GHC/Core/Subst.hs
+++ b/compiler/GHC/Core/Subst.hs
@@ -589,11 +589,13 @@ substDVarSet subst@(Subst _ _ tv_env cv_env) fvs
      = exprFVs fv_expr (const True) emptyVarSet $! acc
 
 ------------------
+-- | Drop free vars from the breakpoint if they have a non-variable substitution.
 substTickish :: Subst -> CoreTickish -> CoreTickish
 substTickish subst (Breakpoint ext n ids modl)
    = Breakpoint ext n (mapMaybe do_one ids) modl
  where
     do_one = getIdFromTrivialExpr_maybe . lookupIdSubst subst
+
 substTickish _subst other = other
 
 {- Note [Substitute lazily]
diff --git a/compiler/GHC/CoreToIface.hs b/compiler/GHC/CoreToIface.hs
index c03be313abc1..02feadd85f2d 100644
--- a/compiler/GHC/CoreToIface.hs
+++ b/compiler/GHC/CoreToIface.hs
@@ -574,7 +574,7 @@ toIfaceTickish (HpcTick modl ix)       = IfaceHpcTick modl ix
 toIfaceTickish (SourceNote src (LexicalFastString names)) =
   IfaceSource src names
 toIfaceTickish (Breakpoint _ ix fv m) =
-  IfaceBreakpoint ix (toIfaceIdBndr <$> fv) m
+  IfaceBreakpoint ix (toIfaceVar <$> fv) m
 
 ---------------------
 toIfaceBind :: Bind Id -> IfaceBinding IfaceLetBndr
diff --git a/compiler/GHC/Iface/Syntax.hs b/compiler/GHC/Iface/Syntax.hs
index f1a1cf8b64c1..ebea95e3ad08 100644
--- a/compiler/GHC/Iface/Syntax.hs
+++ b/compiler/GHC/Iface/Syntax.hs
@@ -635,7 +635,7 @@ data IfaceTickish
   = IfaceHpcTick    Module Int               -- from HpcTick x
   | IfaceSCC        CostCentre Bool Bool     -- from ProfNote
   | IfaceSource  RealSrcSpan FastString      -- from SourceNote
-  | IfaceBreakpoint Int [IfaceIdBndr] Module -- from Breakpoint
+  | IfaceBreakpoint Int [IfaceExpr] Module   -- from Breakpoint
 
 data IfaceAlt = IfaceAlt IfaceConAlt [IfLclName] IfaceExpr
         -- Note: IfLclName, not IfaceBndr (and same with the case binder)
@@ -1844,7 +1844,7 @@ freeNamesIfExpr (IfaceTuple _ as)     = fnList freeNamesIfExpr as
 freeNamesIfExpr (IfaceLam (b,_) body) = freeNamesIfBndr b &&& freeNamesIfExpr body
 freeNamesIfExpr (IfaceApp f a)        = freeNamesIfExpr f &&& freeNamesIfExpr a
 freeNamesIfExpr (IfaceCast e co)      = freeNamesIfExpr e &&& freeNamesIfCoercion co
-freeNamesIfExpr (IfaceTick _ e)       = freeNamesIfExpr e
+freeNamesIfExpr (IfaceTick t e)       = freeNamesIfTickish t &&& freeNamesIfExpr e
 freeNamesIfExpr (IfaceECase e ty)     = freeNamesIfExpr e &&& freeNamesIfType ty
 freeNamesIfExpr (IfaceCase s _ alts)
   = freeNamesIfExpr s &&& fnList fn_alt alts &&& fn_cons alts
@@ -1891,6 +1891,11 @@ freeNamesIfaceTyConParent IfNoParent = emptyNameSet
 freeNamesIfaceTyConParent (IfDataInstance ax tc tys)
   = unitNameSet ax &&& freeNamesIfTc tc &&& freeNamesIfAppArgs tys
 
+freeNamesIfTickish :: IfaceTickish -> NameSet
+freeNamesIfTickish (IfaceBreakpoint _ fvs _) =
+  fnList freeNamesIfExpr fvs
+freeNamesIfTickish _ = emptyNameSet
+
 -- helpers
 (&&&) :: NameSet -> NameSet -> NameSet
 (&&&) = unionNameSet
diff --git a/compiler/GHC/IfaceToCore.hs b/compiler/GHC/IfaceToCore.hs
index c51849029ca0..2150d9a79150 100644
--- a/compiler/GHC/IfaceToCore.hs
+++ b/compiler/GHC/IfaceToCore.hs
@@ -1624,8 +1624,8 @@ tcIfaceTickish (IfaceHpcTick modl ix)   = return (HpcTick modl ix)
 tcIfaceTickish (IfaceSCC  cc tick push) = return (ProfNote cc tick push)
 tcIfaceTickish (IfaceSource src name)   = return (SourceNote src (LexicalFastString name))
 tcIfaceTickish (IfaceBreakpoint ix fvs modl) = do
-  fvs' <- bindIfaceIds fvs pure
-  return (Breakpoint NoExtField ix fvs' modl)
+  fvs' <- mapM tcIfaceExpr fvs
+  return (Breakpoint NoExtField ix [f | Var f <- fvs'] modl)
 
 -------------------------
 tcIfaceLit :: Literal -> IfL Literal
diff --git a/testsuite/tests/ghci/T23612/T23612.hs b/testsuite/tests/ghci/T23612/T23612.hs
new file mode 100644
index 000000000000..e8f478f52932
--- /dev/null
+++ b/testsuite/tests/ghci/T23612/T23612.hs
@@ -0,0 +1,23 @@
+module T23612 where
+
+-- | This will be inlined into @f2@.
+-- Then @a@, @x@, and @y@ will be floated out as constants using @3@ for @a@.
+-- @x@ and @y@ get a breakpoint around the RHS, which is then inlined and
+-- retains a reference to @a@.
+--
+-- Since the actual terms in @x@ and @y@ are now constants, the dependency
+-- analysis for fingerprinting in Recomp doesn't register @a@ as a free variable
+-- anymore.
+-- But when the fingerprints are computed, the breakpoint triggers a lookup of
+-- @a@ (called @f2_a@ then), which fails.
+--
+-- The fix was to include the FVs in the dependencies in @freeNamesIfExpr@.
+-- This has the side effect that the floated out @a@ will still remain in the
+-- program.
+f1 :: Int -> (Int, Int)
+f1 a =
+    let x = a + 1
+        y = a * 2
+    in  (x, y)
+
+f2 = f1 3
diff --git a/testsuite/tests/ghci/T23612/T23612.script b/testsuite/tests/ghci/T23612/T23612.script
new file mode 100644
index 000000000000..930848389221
--- /dev/null
+++ b/testsuite/tests/ghci/T23612/T23612.script
@@ -0,0 +1 @@
+:load T23612
diff --git a/testsuite/tests/ghci/T23612/T23612b.script b/testsuite/tests/ghci/T23612/T23612b.script
new file mode 100644
index 000000000000..da6212754569
--- /dev/null
+++ b/testsuite/tests/ghci/T23612/T23612b.script
@@ -0,0 +1 @@
+:load T23612bB
diff --git a/testsuite/tests/ghci/T23612/T23612bA.hs b/testsuite/tests/ghci/T23612/T23612bA.hs
new file mode 100644
index 000000000000..4c677b5145aa
--- /dev/null
+++ b/testsuite/tests/ghci/T23612/T23612bA.hs
@@ -0,0 +1,5 @@
+module T23612bA where
+
+class C a where
+  c :: a -> a
+  c a = a
diff --git a/testsuite/tests/ghci/T23612/T23612bB.hs b/testsuite/tests/ghci/T23612/T23612bB.hs
new file mode 100644
index 000000000000..9a322c3762b4
--- /dev/null
+++ b/testsuite/tests/ghci/T23612/T23612bB.hs
@@ -0,0 +1,5 @@
+module T23612bB where
+
+import T23612bA
+
+instance C Bool
diff --git a/testsuite/tests/ghci/T23612/all.T b/testsuite/tests/ghci/T23612/all.T
new file mode 100644
index 000000000000..0f14b6490d10
--- /dev/null
+++ b/testsuite/tests/ghci/T23612/all.T
@@ -0,0 +1,2 @@
+test('T23612', only_ways(['ghci-opt']), ghci_script, ['T23612.script'])
+test('T23612b', [only_ways(['ghci-opt']), extra_files(['T23612bA.hs', 'T23612bB.hs'])], ghci_script, ['T23612b.script'])
-- 
GitLab