Marge Bot pushed to branch wip/marge_bot_batch_merge_job at Glasgow Haskell Compiler / GHC
Commits:
-
40764930
by sheaf at 2026-06-12T14:54:43-04:00
-
0f3d0a71
by Zubin Duggal at 2026-06-12T14:55:30-04:00
-
acfc184b
by mangoiv at 2026-06-12T15:28:59-04:00
-
d1b8a9c2
by David Eichmann at 2026-06-12T15:28:59-04:00
-
9426fd1f
by David Eichmann at 2026-06-12T15:29:00-04:00
16 changed files:
- .gitlab-ci.yml
- + changelog.d/tool-messages-27370
- compiler/GHC/SysTools/Process.hs
- compiler/GHC/Tc/Solver/Equality.hs
- compiler/GHC/Tc/Solver/FunDeps.hs
- libraries/process
- testsuite/driver/testlib.py
- + testsuite/tests/driver/T27370/Makefile
- + testsuite/tests/driver/T27370/T27370.hs
- + testsuite/tests/driver/T27370/T27370.pp
- + testsuite/tests/driver/T27370/T27370.stderr
- + testsuite/tests/driver/T27370/all.T
- + testsuite/tests/perf/compiler/T26426.hs
- testsuite/tests/perf/compiler/all.T
- utils/ghc-toolchain/src/GHC/Toolchain/Program.hs
- utils/ghc-toolchain/src/GHC/Toolchain/Utils.hs
Changes:
| ... | ... | @@ -255,6 +255,9 @@ lint-changelog: |
| 255 | 255 | BUILD_FLAVOUR: default
|
| 256 | 256 | CHANGELOG_EXPECT_MR: "$CI_MERGE_REQUEST_IID"
|
| 257 | 257 | script:
|
| 258 | + # Cancel the job if there is a no-changelog label
|
|
| 259 | + - |
|
|
| 260 | + [[ ",${CI_MERGE_REQUEST_LABELS}," == *",no-changelog,"* ]] && exit 0
|
|
| 258 | 261 | # Check that the MR adds at least one changelog entry
|
| 259 | 262 | - git fetch "$CI_MERGE_REQUEST_PROJECT_URL" "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
| 260 | 263 | - base="$(git merge-base FETCH_HEAD $CI_COMMIT_SHA)"
|
| ... | ... | @@ -274,8 +277,6 @@ lint-changelog: |
| 274 | 277 | rules:
|
| 275 | 278 | - if: '$CI_MERGE_REQUEST_LABELS =~ /.*marge_bot_batch_merge_job.*/'
|
| 276 | 279 | when: never
|
| 277 | - - if: '$CI_MERGE_REQUEST_LABELS =~ /.*no-changelog.*/'
|
|
| 278 | - when: never
|
|
| 279 | 280 | - if: $CI_MERGE_REQUEST_ID
|
| 280 | 281 | - *drafts-can-fail-lint
|
| 281 | 282 |
| 1 | +section: compiler
|
|
| 2 | +synopsis: Mark messages from external tools as errors or warnings depending on
|
|
| 3 | + the tool's exit code. Previously, output printed to stderr by tools was
|
|
| 4 | + unconditionally reported as errors.
|
|
| 5 | +issues: #27370
|
|
| 6 | +mrs: !16170 |
| ... | ... | @@ -26,8 +26,9 @@ import GHC.Utils.Logger |
| 26 | 26 | import GHC.Utils.TmpFs
|
| 27 | 27 | import GHC.Utils.CliOption
|
| 28 | 28 | |
| 29 | -import GHC.Driver.Errors (reportError)
|
|
| 29 | +import GHC.Driver.Errors (reportDiagnostic)
|
|
| 30 | 30 | |
| 31 | +import GHC.Types.Error ( DiagnosticReason(..) )
|
|
| 31 | 32 | import GHC.Types.SrcLoc ( SrcLoc, mkSrcLoc, mkSrcSpan )
|
| 32 | 33 | import GHC.Data.FastString
|
| 33 | 34 | |
| ... | ... | @@ -271,9 +272,10 @@ builderMainLoop logger filter_fn pgm real_args mb_cwd mb_env = withPipe $ \ (rea |
| 271 | 272 | getLocaleEncoding >>= hSetEncoding readEnd
|
| 272 | 273 | hSetNewlineMode readEnd nativeNewlineMode
|
| 273 | 274 | hSetBuffering readEnd LineBuffering
|
| 274 | - messages <- parseBuildMessages . filter_fn . lines <$> hGetContents readEnd
|
|
| 275 | - mapM_ processBuildMessage messages
|
|
| 276 | - waitForProcess hProcess
|
|
| 275 | + messages <- parseBuildMessages . filter_fn . lines <$> hGetContents' readEnd
|
|
| 276 | + code <- waitForProcess hProcess
|
|
| 277 | + mapM_ (processBuildMessage code) messages
|
|
| 278 | + return code
|
|
| 277 | 279 | hClose hStdIn
|
| 278 | 280 | case r of
|
| 279 | 281 | Left (SomeException e) -> do
|
| ... | ... | @@ -282,13 +284,16 @@ builderMainLoop logger filter_fn pgm real_args mb_cwd mb_env = withPipe $ \ (rea |
| 282 | 284 | Right s -> do
|
| 283 | 285 | return s
|
| 284 | 286 | where
|
| 285 | - processBuildMessage :: BuildMessage -> IO ()
|
|
| 286 | - processBuildMessage msg = do
|
|
| 287 | + processBuildMessage :: ExitCode -> BuildMessage -> IO ()
|
|
| 288 | + processBuildMessage code msg = do
|
|
| 287 | 289 | case msg of
|
| 288 | 290 | BuildMsg msg -> do
|
| 289 | 291 | logInfo logger $ withPprStyle defaultUserStyle msg
|
| 290 | 292 | BuildError loc msg -> do
|
| 291 | - reportError logger neverQualify emptyDiagOpts (mkSrcSpan loc loc) msg
|
|
| 293 | + let reason = case code of
|
|
| 294 | + ExitSuccess -> WarningWithoutFlag
|
|
| 295 | + ExitFailure{} -> ErrorWithoutFlag
|
|
| 296 | + reportDiagnostic logger neverQualify emptyDiagOpts (mkSrcSpan loc loc) reason msg
|
|
| 292 | 297 | |
| 293 | 298 | parseBuildMessages :: [String] -> [BuildMessage]
|
| 294 | 299 | parseBuildMessages str = loop str Nothing
|
| ... | ... | @@ -2980,15 +2980,30 @@ But it's not so simple: |
| 2980 | 2980 | we kick out g1. Now we have two constraints
|
| 2981 | 2981 | [W] g1 : F a ~ a Int (arising from (F a ~ a Int))
|
| 2982 | 2982 | [W] g2{rw:g1} : F a ~ a Int (arising from (F alpha ~ F a))
|
| 2983 | - If we end up with g2 in the inert set (not g1) we'll get a very confusing
|
|
| 2984 | - error message that we can solve (F a ~ a Int)
|
|
| 2985 | - arising from F a ~ F a
|
|
| 2983 | + If we solve `g1` from `g2` we end up with
|
|
| 2984 | + g1 := g2
|
|
| 2985 | + [W] g2{} : F a ~ a Int (arising from (F alpha ~ F a))
|
|
| 2986 | + and hence (since alpha := a) we report that we can't solve (F a ~ a Int)
|
|
| 2987 | + arising from (F a ~ F a), which is extremely confusing. Moreover, it seems
|
|
| 2988 | + wrong to "solve" `g1` using `g2` when `g2` has itself been rewritten by `g1`!
|
|
| 2986 | 2989 | |
| 2987 | 2990 | TL;DR: Better to hang on to `g1` (with no rewriters), in preference
|
| 2988 | 2991 | to `g2` (which has a rewriter).
|
| 2989 | 2992 | |
| 2990 | 2993 | See (WRW11) in Note [Wanteds rewrite Wanteds: rewriter-sets]
|
| 2991 | 2994 | in GHC.Tc.Types.Constraint.
|
| 2995 | + |
|
| 2996 | + Note that the decision to prefer a constraint without rewriters over one that
|
|
| 2997 | + has rewriters can also have a /huge/ effect on performance. For instance, it
|
|
| 2998 | + avoids an **exponential** blow-up in the size of coercions produced when
|
|
| 2999 | + typechecking in T26426. In that program, we have coercions of the form:
|
|
| 3000 | + |
|
| 3001 | + co_i :: TaggedTypes as `Append` TaggedTypes '[ty]
|
|
| 3002 | + ~# TaggedTypes (as `Append` '[ty])
|
|
| 3003 | + |
|
| 3004 | + and each 'co_{i+1}' contains the previous 'co_i' twice. Without preferring
|
|
| 3005 | + Wanteds with no rewriters, we essentially end up inlining 'co_i' into 'co_{i+1}',
|
|
| 3006 | + which results in exponentially-sized proof terms, growing like O(2^i).
|
|
| 2992 | 3007 | -}
|
| 2993 | 3008 | |
| 2994 | 3009 | tryInertEqs :: EqCt -> SolverStage ()
|
| ... | ... | @@ -1198,6 +1198,56 @@ Key point: equations that are not relevant do not need to be considered for fund |
| 1198 | 1198 | and [W] I alpha ~ Int |> g2
|
| 1199 | 1199 | Here we definiteily want to take advantage of injectivity.
|
| 1200 | 1200 | |
| 1201 | +(CF6) This machinery can also have a significant positive effect on the size of
|
|
| 1202 | + proof terms. For example (simplification of T26426):
|
|
| 1203 | + |
|
| 1204 | + type family (++) a b where { '[] ++ ys = ys; (x:xs) ++ ys = x : (xs ++ ys) }
|
|
| 1205 | + type family MapId a where { MapId '[] = '[]; MapId (x:xs) = x : MapId xs }
|
|
| 1206 | + |
|
| 1207 | + app :: (MapId xs ++ MapId ys ~ MapId (xs ++ ys)) => Proxy xs -> Proxy ys -> Proxy (xs ++ ys)
|
|
| 1208 | + |
|
| 1209 | + test :: Proxy [ty_1, ..., ty_n]
|
|
| 1210 | + test = Proxy @'[ty_1]
|
|
| 1211 | + `app` Proxy @'[ty_2]
|
|
| 1212 | + ...
|
|
| 1213 | + `app` Proxy @'[ty_n]
|
|
| 1214 | + |
|
| 1215 | + Every `app` call gives rise to a Wanted of the form:
|
|
| 1216 | + |
|
| 1217 | + [W] MapId acc_i ++ MapId '[ty_i] ~ MapId (acc_i ++ '[ty_i])
|
|
| 1218 | + |
|
| 1219 | + while the overall result type gives us a Wanted of the form
|
|
| 1220 | + |
|
| 1221 | + [W] acc_n ++ '[ty_n] ~ [ty_1, ..., ty_n]
|
|
| 1222 | + |
|
| 1223 | + By using (CFFA) on this result Wanted, we deduce that we must have
|
|
| 1224 | + |
|
| 1225 | + acc_n ~ [ty_1, ..., ty_{n-1}]
|
|
| 1226 | + |
|
| 1227 | + which is a flat list. Repeating the process, (CFFA) allows us to deduce that
|
|
| 1228 | + |
|
| 1229 | + acc_i ~ [ty_1, ..., ty_{i-1}]
|
|
| 1230 | + |
|
| 1231 | + for all i. This allows the other Wanteds to be solved directly, giving rise to
|
|
| 1232 | + proof terms with the typical triangular O(n^2) shape
|
|
| 1233 | + |
|
| 1234 | + co_i = (O(i) proof that MapId acc_i ++ MapId '[ty_i] ~ acc_{i+1})
|
|
| 1235 | + ; (O(i) proof that acc_{i+1} ~ MapId (acc_i ++ '[ty_i]))
|
|
| 1236 | + |
|
| 1237 | + However, /without/ (CFFA), 'acc_i' is not unified with a flat list but is left
|
|
| 1238 | + as the nested application:
|
|
| 1239 | + |
|
| 1240 | + acc_i ~ (... (('[ty_1] ++ '[ty_2]) ++ '[ty_3]) ... ++ '[ty_{i-1}])
|
|
| 1241 | + |
|
| 1242 | + This means that 'MapId acc_i' is stuck until we reduce the above, which takes
|
|
| 1243 | + O(i^2) type family reduction steps, instead of O(i). The same applies to
|
|
| 1244 | + the other proof term involving 'MapId (acc_i ++ '[ty_i])'.
|
|
| 1245 | + Consequently, without (CFFA) the overall coercion size blows up to O(n^3).
|
|
| 1246 | + |
|
| 1247 | + The takeaway is that (CFFA) allows us to push in the (flat) result type,
|
|
| 1248 | + instead of relying on recursively built sub-proof terms, which brings down
|
|
| 1249 | + coercion sizes (in certain situations) from O(n^3) to O(n^2).
|
|
| 1250 | + |
|
| 1201 | 1251 | Note [Cache-caused loops]
|
| 1202 | 1252 | ~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 1203 | 1253 | It is very dangerous to cache a rewritten wanted family equation as 'solved' in
|
| 1 | -Subproject commit 92deb52c1781bf10ad390296dbc435abe103bfe4 |
|
| 1 | +Subproject commit 11fd247ad33208da7a914acf15d4a09d64a6a4c4 |
| ... | ... | @@ -3066,11 +3066,15 @@ def normalise_errmsg(s: str) -> str: |
| 3066 | 3066 | # Old emcc warns when we export HEAP8 but new one requires it (see #26290)
|
| 3067 | 3067 | s = s.replace('warning: invalid item in EXPORTED_RUNTIME_METHODS: HEAP8\nwarning: invalid item in EXPORTED_RUNTIME_METHODS: HEAPU8\nemcc: warning: warnings in JS library compilation [-Wjs-compiler]\n','')
|
| 3068 | 3068 | |
| 3069 | - # on newer versions of MacOS X, the shipped ranlib warns about object files with no symbols,
|
|
| 3070 | - # however, these are completely benign stubs.
|
|
| 3071 | - # See https://gitlab.haskell.org/ghc/ghc/-/issues/27116
|
|
| 3072 | 3069 | if opsys('darwin'):
|
| 3070 | + # on newer versions of MacOS X, the shipped ranlib warns about object files with no symbols,
|
|
| 3071 | + # however, these are completely benign stubs.
|
|
| 3072 | + # See https://gitlab.haskell.org/ghc/ghc/-/issues/27116
|
|
| 3073 | 3073 | s = modify_lines(s, lambda l: re.sub(r'.*ranlib:.*has no symbols', '', l))
|
| 3074 | + # we also want to remove linker warnings having to do with undefined dynamic_lookup in combination with
|
|
| 3075 | + # making a single weak symbol undefined as this is dependent on other linker flags
|
|
| 3076 | + # See also https://github.com/haskell/process/pull/377
|
|
| 3077 | + s = drop_lines_containing(s, 'ld: warning: -U option is redundant when using -undefined dynamic_lookup')
|
|
| 3074 | 3078 | |
| 3075 | 3079 | return s
|
| 3076 | 3080 |
| 1 | +TOP=../../..
|
|
| 2 | +include $(TOP)/mk/boilerplate.mk
|
|
| 3 | +include $(TOP)/mk/test.mk
|
|
| 4 | + |
|
| 5 | +T27370:
|
|
| 6 | + chmod +x ./T27370.pp
|
|
| 7 | + '$(TEST_HC)' $(TEST_HC_OPTS) -c T27370.hs |
| 1 | +{-# OPTIONS_GHC -F -pgmF ./T27370.pp #-}
|
|
| 2 | +module T27370 where |
| 1 | +#!/bin/sh
|
|
| 2 | +cp "$2" "$3"
|
|
| 3 | +echo "$1:2:8: a located warning from an external tool"
|
|
| 4 | +echo " with a continuation line"
|
|
| 5 | +echo "an unlocated line from an external tool" |
| 1 | +T27370.hs:2:8: warning:
|
|
| 2 | + a located warning from an external tool
|
|
| 3 | + with a continuation line
|
|
| 4 | + |
|
| 5 | +an unlocated line from an external tool |
| 1 | +test('T27370',
|
|
| 2 | + [extra_files(['T27370.hs', 'T27370.pp']),
|
|
| 3 | + when(opsys('mingw32'), skip)],
|
|
| 4 | + makefile_test, ['T27370']) |
| 1 | +{-# LANGUAGE DataKinds #-}
|
|
| 2 | +{-# LANGUAGE TypeFamilies #-}
|
|
| 3 | +module T26426 (
|
|
| 4 | + someTypes
|
|
| 5 | +) where
|
|
| 6 | + |
|
| 7 | +import Data.Kind (Type)
|
|
| 8 | +import GHC.TypeLits (Symbol)
|
|
| 9 | + |
|
| 10 | +type family Append (left :: [k]) (right :: [k]) :: [k] where
|
|
| 11 | + Append '[] right = right
|
|
| 12 | + Append (a : rest) right = a : Append rest right
|
|
| 13 | + |
|
| 14 | +type family TaggedTypes (tags :: [(Symbol, Type)]) :: [Type] where
|
|
| 15 | + TaggedTypes '[] = '[]
|
|
| 16 | + TaggedTypes ('(_, typ) : rest) = typ : TaggedTypes rest
|
|
| 17 | + |
|
| 18 | +data Types (types :: [(Symbol, Type)]) = Types
|
|
| 19 | + |
|
| 20 | +mkTypes :: forall sym val. val -> Types '[ '(sym, val) ]
|
|
| 21 | +mkTypes _ = Types
|
|
| 22 | + |
|
| 23 | +appendTypes ::
|
|
| 24 | + -- This constraint is the one that causes the issue. If the next line is commented
|
|
| 25 | + -- out, then this module compiles quickly
|
|
| 26 | + Append (TaggedTypes left) (TaggedTypes right) ~ TaggedTypes (Append left right) =>
|
|
| 27 | + Types left -> Types right -> Types (Append left right)
|
|
| 28 | +appendTypes _ _ = Types
|
|
| 29 | + |
|
| 30 | +someTypes ::
|
|
| 31 | + Types
|
|
| 32 | + [ '("01", Int)
|
|
| 33 | + , '("02", Int)
|
|
| 34 | + , '("03", Int)
|
|
| 35 | + , '("04", Int)
|
|
| 36 | + , '("05", Int)
|
|
| 37 | + , '("06", Int)
|
|
| 38 | + , '("07", Int)
|
|
| 39 | + , '("08", Int)
|
|
| 40 | + , '("09", Int)
|
|
| 41 | + , '("10", Int)
|
|
| 42 | + , '("11", Int)
|
|
| 43 | + , '("12", Int)
|
|
| 44 | + , '("13", Int)
|
|
| 45 | + , '("14", Int)
|
|
| 46 | + , '("15", Int)
|
|
| 47 | + , '("16", Int)
|
|
| 48 | + ]
|
|
| 49 | + |
|
| 50 | +someTypes =
|
|
| 51 | + mkTypes @"01" 1 `appendTypes`
|
|
| 52 | + mkTypes @"02" 2 `appendTypes`
|
|
| 53 | + mkTypes @"03" 3 `appendTypes`
|
|
| 54 | + mkTypes @"04" 4 `appendTypes`
|
|
| 55 | + mkTypes @"05" 5 `appendTypes`
|
|
| 56 | + mkTypes @"06" 6 `appendTypes`
|
|
| 57 | + mkTypes @"07" 7 `appendTypes`
|
|
| 58 | + mkTypes @"08" 8 `appendTypes`
|
|
| 59 | + mkTypes @"09" 9 `appendTypes`
|
|
| 60 | + mkTypes @"10" 10 `appendTypes`
|
|
| 61 | + mkTypes @"11" 11 `appendTypes`
|
|
| 62 | + mkTypes @"12" 12 `appendTypes`
|
|
| 63 | + mkTypes @"13" 13 `appendTypes`
|
|
| 64 | + mkTypes @"14" 14 `appendTypes`
|
|
| 65 | + mkTypes @"15" 15 `appendTypes`
|
|
| 66 | + mkTypes @"16" 16 |
| ... | ... | @@ -87,6 +87,13 @@ test('T783', |
| 87 | 87 | ],
|
| 88 | 88 | compile,[''])
|
| 89 | 89 | |
| 90 | +test ('T26426'
|
|
| 91 | + , [ only_ways(['normal'])
|
|
| 92 | + , collect_compiler_stats('bytes allocated',4) ]
|
|
| 93 | + , compile
|
|
| 94 | + , ['']
|
|
| 95 | + )
|
|
| 96 | + |
|
| 90 | 97 | test('T5321Fun',
|
| 91 | 98 | [ only_ways(['normal']), # no optimisation for this one
|
| 92 | 99 | collect_compiler_runtime(2),
|
| ... | ... | @@ -148,12 +148,14 @@ findProgram description userSpec candidates |
| 148 | 148 | Just prefix -> map (prefix++) candidates
|
| 149 | 149 | Nothing -> []
|
| 150 | 150 | candidates' = prefixedCandidates ++ candidates
|
| 151 | - err =
|
|
| 152 | - [ "Failed to find " ++ description ++ "."
|
|
| 153 | - , "Looked for one of " ++ show candidates' ++ " in the system search path."
|
|
| 154 | - ]
|
|
| 155 | - path <- oneOf' err (map findExecutableErr candidates')
|
|
| 156 | - return Program { prgPath = path, prgFlags = fromMaybe [] (poFlags userSpec) }
|
|
| 151 | + pathMay <- findM doesExecutableExist candidates'
|
|
| 152 | + case pathMay of
|
|
| 153 | + Nothing -> throwEs
|
|
| 154 | + [ "Failed to find " ++ description ++ "."
|
|
| 155 | + , "Looked for one of " ++ show candidates' ++ " in the system search path."
|
|
| 156 | + ]
|
|
| 157 | + Just path ->
|
|
| 158 | + return Program { prgPath = path, prgFlags = fromMaybe [] (poFlags userSpec) }
|
|
| 157 | 159 | |
| 158 | 160 | -- Note that @configure.ac@ checks these llvm version constants (using @sed@) to
|
| 159 | 161 | -- ensure they are the same as the @$LlvmMinVersion@ and @$LlvmMaxVersion@
|
| ... | ... | @@ -222,21 +224,18 @@ maybeFindProgramFromProgOpts :: String -> ProgOpt -> Maybe (M Program) |
| 222 | 224 | maybeFindProgramFromProgOpts description userSpec = case poPath userSpec of
|
| 223 | 225 | Nothing -> Nothing
|
| 224 | 226 | Just path -> Just $ do
|
| 225 | - let err =
|
|
| 226 | - [ "Failed to find " ++ description ++ "."
|
|
| 227 | - , "Looked for user-specified program '" ++ path ++ "' in the system search path."
|
|
| 228 | - ]
|
|
| 229 | - path' <- findExecutableErr path <|> throwEs err
|
|
| 230 | - return Program { prgPath = path', prgFlags = fromMaybe [] (poFlags userSpec) }
|
|
| 231 | - |
|
| 232 | -findExecutableErr :: String -> M FilePath
|
|
| 233 | -findExecutableErr name = do
|
|
| 234 | - r <- liftIO $ findExecutable name
|
|
| 235 | - case r of
|
|
| 236 | - Nothing -> throwE $ name ++ " not found in search path"
|
|
| 237 | - -- Use the given `prgPath` or candidate name rather than the
|
|
| 238 | - -- absolute path returned by `findExecutable`.
|
|
| 239 | - Just _x -> return name
|
|
| 227 | + exists <- doesExecutableExist path
|
|
| 228 | + unless exists $ throwEs
|
|
| 229 | + [ "Failed to find " ++ description ++ "."
|
|
| 230 | + , "Looked for user-specified program '" ++ path ++ "' in the system search path."
|
|
| 231 | + ]
|
|
| 232 | + return Program { prgPath = path, prgFlags = fromMaybe [] (poFlags userSpec) }
|
|
| 233 | + |
|
| 234 | +doesExecutableExist
|
|
| 235 | + :: String -- ^ executable name
|
|
| 236 | + -> M Bool
|
|
| 237 | +doesExecutableExist name = isJust <$> liftIO (findExecutable name)
|
|
| 238 | + |
|
| 240 | 239 | |
| 241 | 240 | -------------------- Compiling utilities --------------------
|
| 242 | 241 |
| ... | ... | @@ -9,6 +9,7 @@ module GHC.Toolchain.Utils |
| 9 | 9 | , oneOf'
|
| 10 | 10 | , isSuccess
|
| 11 | 11 | , lastLine
|
| 12 | + , findM
|
|
| 12 | 13 | ) where
|
| 13 | 14 | |
| 14 | 15 | import Control.Exception
|
| ... | ... | @@ -69,3 +70,10 @@ isSuccess = \case |
| 69 | 70 | |
| 70 | 71 | lastLine :: String -> String
|
| 71 | 72 | lastLine = maybe "" snd . unsnoc . lines
|
| 73 | + |
|
| 74 | +findM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)
|
|
| 75 | +findM f = \case
|
|
| 76 | + [] -> return Nothing
|
|
| 77 | + a:as -> do
|
|
| 78 | + found <- f a
|
|
| 79 | + if found then return (Just a) else findM f as |