Visible rules unrelated to module can affect its optimisation/ the simplifier
## Summary The rules which are loaded in the EPS, even if they are not part of the dependency closure of a given module, affect the outcome of rebuilding a call in the simplifier. Because the rules are loaded non-deterministically, this means the simplifier can non deterministically produce different results, which breaks interface and object determinism. In our example, this function application: ``` (GHC.Classes.< @GHC.Num.Integer.Integer GHC.Num.Integer.$fOrdInteger inp'_a18mlV (Data.Convertible.Base.convert @b_a18mkc @GHC.Num.Integer.Integer $dConvertible_a18mk6 smallest_a18moQ)) ``` by the time it's printed by `-ddump-rules-rewrites` can either look like ``` Rule fired Rule: Class op < Module: (BUILTIN) Before: GHC.Classes.< TyArg GHC.Num.Integer.Integer ValArg GHC.Num.Integer.$fOrdInteger After: GHC.Num.Integer.integerLt Cont: ApplyToVal nodup hole GHC.Num.Integer.Integer -> GHC.Num.Integer.Integer -> GHC.Types.Bool inp'_a18mlV ApplyToVal nodup hole GHC.Num.Integer.Integer -> GHC.Types.Bool (Data.Convertible.Base.convert @b_a18mkc @GHC.Num.Integer.Integer $dConvertible_a18mk6 smallest_a18moQ) Select nodup wild_a18lSg Select nodup wild_00 Stop[BoringCtxt] GHC.Internal.Data.Either.Either Data.Convertible.Base.ConvertError b_a18mkc ``` *or* ``` Rule fired Rule: Class op < Module: (BUILTIN) Before: GHC.Classes.< TyArg GHC.Num.Integer.Integer ValArg GHC.Num.Integer.$fOrdInteger ValArg inp'_a2iJ ValArg case ($dConvertible_a2rN `cast` (Data.Convertible.Base.N:Convertible[0] <b_a2rC>_N <GHC.Num.Integer.Integer>_N :: Data.Convertible.Base.Convertible b_a2rC GHC.Num.Integer.Integer ~R# (b_a2rC -> Data.Convertible.Base.ConvertResult GHC.Num.Integer.Integer))) smallest_a2if of { GHC.Internal.Data.Either.Left e_i3gv -> Data.Convertible.Base.convert1 @GHC.Num.Integer.Integer e_i3gv; GHC.Internal.Data.Either.Right r_i3gH -> r_i3gH } After: GHC.Num.Integer.integerLt [<as above...>] Cont: Select nodup wild_a3iH Select nodup wild_00 Stop[BoringCtxt] GHC.Internal.Data.Either.Either Data.Convertible.Base.ConvertError b_a2rC ``` ## Diagnosis The `<` function has arity 4. On the first outcome, only the first two arguments (`TyArg GHC.Num.Integer.Integer` and `ValArg GHC.Num.Integer.$fOrdInteger`) are moved from the continuation back to the resulting expression when rebuilding. On the second outcome, all four arguments are moved out from the continuation. The cause of this is discrepancy is the `nr_wanted == 0` check in: ```haskell rebuildCall env info@(ArgInfo { ai_fun = fun, ai_args = rev_args , ai_rewrite = TryRules nr_wanted rules }) cont | nr_wanted == 0 || no_more_args ``` `nr_wanted` is the maximum arity of any given loaded visible rule. In the first outcome, `nr_wanted == 2`, so after moving the first two arguments we stop. In contrast, for the second outcome, `nr_wanted == 4` so we ended up moving all the arguments. Tracing `no_more_args` and `rules` shows this for the first outcome: ``` False [Built in rule for <: "Class op <"] ``` And for the second, note how there are more visible rules whose arity is 4 rather than the arity 2 of the built in rule for `<`: ``` no_more_args: True rules: ["ByteString.Lazy length/<N -> compareLength/==LT" [~1] forall ($dOrd_a18lwL :: Ord Int64) (t_a18lkw :: ByteString) (n_a18liP :: Int64). < @Int64 $dOrd_a18lwL (length t_a18lkw) n_a18liP = case compareLength t_a18lkw n_a18liP of { __DEFAULT -> False; LT -> True }, "ByteString.Lazy <N/length -> compareLength/==GT" [~1] forall ($dOrd_a18liK :: Ord Int64) (t_a18liJ :: ByteString) (n_a18liI :: Int64). < @Int64 $dOrd_a18liK n_a18liI (length t_a18liJ) = case compareLength t_a18liJ n_a18liI of { __DEFAULT -> False; GT -> True }, Built in rule for <: "Class op <"] ``` ## Steps to reproduce The example we've been looking at is `Data.Convertible.Utils` from the `convertible` package. ``` cabal get convertible cd convertible cabal build -w <ghc-HEAD> mv dist-newstyle first-dist-newstyle cabal build -w <ghc-HEAD> diff --recursive dist-newstyle first-dist-newstyle ``` If you get no differences in interface files, remove `dist-newstyle` and try again until you see something like ``` Files /var/folders/tv/35hlch6s3y15hfvndc71l6d40000gn/T/tmp.wOCHDBAqKF/convertible-1.1.1.1/dist-newstyle/build/aarch64-osx/ghc-9.11.20240807/convertible-1.1.1.1/build/Data/Convertible.hi and /var/folders/tv/35hlch6s3y15hfvndc71l6d40000gn/T/tmp.wOCHDBAqKF/convertible-1.1.1.1/first-dist-newstyle/build/aarch64-osx/ghc-9.11.20240807/convertible-1.1.1.1/build/Data/Convertible.hi differ ``` Then use the usual `ghc-HEAD --show-iface <.hi>` to get a human readable output out of the differing files. ## Expected behavior The output of the simplifier should be stable to guarantee interface and object determinism. ## Environment * GHC version used: HEAD (this is an issue exposed only after 9.10) @simonpj We would appreciate your input on what you think we should do about this
issue