Skip to content

Commit eba6fa1

Browse files
jmcardonemilypi
andauthored
Forked errors on chain (#1185)
* wip fix args error * more forked errors * fix tests * fix tests * remove top level native docs on chain * Add tests for env-in-repl doc protections * forking more errors * remove bool blindness * change in-repl function name * fork native docs * env simulate on chain * Address John's comments --------- Co-authored-by: Emily Pillmore <[email protected]>
1 parent 10a95cf commit eba6fa1

File tree

10 files changed

+376
-42
lines changed

10 files changed

+376
-42
lines changed

docs/en/pact-functions.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -1916,7 +1916,7 @@ Retreive any accumulated events and optionally clear event state. Object returne
19161916
*&rarr;*&nbsp;`[string]`
19171917

19181918

1919-
Queries, or with arguments, sets execution config flags. Valid flags: ["AllowReadInLocal","DisableHistoryInTransactionalMode","DisableInlineMemCheck","DisableModuleInstall","DisableNewTrans","DisablePact40","DisablePact420","DisablePact43","DisablePact431","DisablePact44","DisablePact45","DisablePact46","DisablePactEvents","EnforceKeyFormats","OldReadOnlyBehavior","PreserveModuleIfacesBug","PreserveModuleNameBug","PreserveNsModuleInstallBug","PreserveShowDefs"]
1919+
Queries, or with arguments, sets execution config flags. Valid flags: ["AllowReadInLocal","DisableHistoryInTransactionalMode","DisableInlineMemCheck","DisableModuleInstall","DisableNewTrans","DisablePact40","DisablePact420","DisablePact43","DisablePact431","DisablePact44","DisablePact45","DisablePact46","DisablePact47","DisablePactEvents","EnforceKeyFormats","OldReadOnlyBehavior","PreserveModuleIfacesBug","PreserveModuleNameBug","PreserveNsModuleInstallBug","PreserveShowDefs"]
19201920
```lisp
19211921
pact> (env-exec-config ['DisableHistoryInTransactionalMode]) (env-exec-config)
19221922
["DisableHistoryInTransactionalMode"]
@@ -2039,6 +2039,17 @@ Set transaction signature keys and capabilities. SIGS is a list of objects with
20392039
```
20402040

20412041

2042+
### env-simulate-onchain {#env-simulate-onchain}
2043+
2044+
*on-chain*&nbsp;`bool` *&rarr;*&nbsp;`string`
2045+
2046+
2047+
Set a flag to simulate on-chain behavior that differs from the repl, in particular for observing things like errors and stack traces.
2048+
```lisp
2049+
(env-simulate-onchain true)
2050+
```
2051+
2052+
20422053
### expect {#expect}
20432054

20442055
*doc*&nbsp;`string` *expected*&nbsp;`<a>` *actual*&nbsp;`<a>` *&rarr;*&nbsp;`string`

src/Pact/Eval.hs

+23-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module Pact.Eval
3333
(eval
3434
,evalBeginTx,evalRollbackTx,evalCommitTx
3535
,reduce,reduceBody
36+
,reduceEnscoped
3637
,resolveFreeVars,resolveArg,resolveRef
3738
,enforceKeySet,enforceKeySetName
3839
,enforceGuard
@@ -346,7 +347,19 @@ eval' (TModule _tm@(MDInterface m) bod i) =
346347
writeRow i Write Modules (_interfaceName mangledI) =<< traverse (traverse toPersistDirect') govI
347348
endAdvice govI
348349
return (g, msg $ "Loaded interface " <> pretty (_interfaceName mangledI))
349-
eval' t = enscope t >>= reduce
350+
eval' t = enscope t >>= reduceEnscoped
351+
352+
reduceEnscoped :: Term Ref -> Eval e (Term Name)
353+
reduceEnscoped = \case
354+
TVar (Direct t'@TNative{}) i ->
355+
isOffChainForkedError >>= \case
356+
OnChainError -> evalError' i "Cannot display native function details in non-repl context"
357+
OffChainError -> pure t'
358+
TVar (Ref t'@TDef{}) i ->
359+
isOffChainForkedError >>= \case
360+
OnChainError -> evalError' i "Cannot display function details in non-repl context"
361+
OffChainError -> toTerm <$> compatPretty t'
362+
t' -> reduce t'
350363

351364
-- | Enforce namespace/root access on install.
352365
enforceNamespaceInstall
@@ -1037,8 +1050,14 @@ reduce t@TLiteral {} = unsafeReduce t
10371050
reduce t@TGuard {} = unsafeReduce t
10381051
reduce TLam{..} = evalError _tInfo "Cannot reduce bound lambda"
10391052
reduce TList {..} = TList <$> mapM reduce _tList <*> traverse reduce _tListType <*> pure _tInfo
1040-
reduce t@TDef {} = toTerm <$> compatPretty t
1041-
reduce t@TNative {} = toTerm <$> compatPretty t
1053+
reduce t@TDef {} =
1054+
isExecutionFlagSet FlagDisablePact47 >>= \case
1055+
True -> toTerm <$> compatPretty t
1056+
False -> evalError' (_tInfo t) "Cannot display function details in non-repl context"
1057+
reduce t@TNative {} =
1058+
isExecutionFlagSet FlagDisablePact47 >>= \case
1059+
True -> toTerm <$> compatPretty t
1060+
False -> evalError' (_tInfo t) "Cannot display native function details in non-repl context"
10421061
reduce TConst {..} = case _tConstVal of
10431062
CVEval _ t -> reduce t
10441063
CVRaw a -> evalError _tInfo $ "internal error: reduce: unevaluated const: " <> pretty a
@@ -1053,7 +1072,7 @@ reduce t@TStep {} = evalError (_tInfo t) "Step at invalid location"
10531072
reduce TSchema {..} = TSchema _tSchemaName _tModule _tMeta <$> traverse (traverse reduce) _tFields <*> pure _tInfo
10541073
reduce TTable {..} = TTable _tTableName _tModuleName _tHash <$> mapM reduce _tTableType <*> pure _tMeta <*> pure _tInfo
10551074
reduce t@TModRef{} = unsafeReduce t
1056-
reduce (TDynamic tref tmem i) = reduceDynamic tref tmem i >>= \rd -> case rd of
1075+
reduce (TDynamic tref tmem i) = reduceDynamic tref tmem i >>= \case
10571076
Left v -> return v
10581077
Right d -> reduce (TDef d (getInfo d))
10591078

src/Pact/Native.hs

+30-12
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ enforceDef = defNative "enforce" enforce
164164
return (TLiteral (LBool True) def)
165165
else reduce msg >>= \case
166166
TLitString msg' -> failTx (_faInfo i) $ pretty msg'
167-
e -> evalError' i $ "Invalid message argument, expected string " <> pretty e
167+
e -> isOffChainForkedError >>= \case
168+
OffChainError -> evalError' i $ "Invalid message argument, expected string " <> pretty e
169+
OnChainError -> evalError' i $ "Invalid message argument, expected string, received argument of type: " <> pretty (typeof' e)
168170
cond' -> reduce msg >>= argsError i . reverse . (:[cond'])
169171
enforceLazy i as = mapM reduce as >>= argsError i
170172

@@ -344,8 +346,11 @@ ifDef = defNative "if" if' (funType a [("cond",tTyBool),("then",a),("else",a)])
344346

345347
if' :: NativeFun e
346348
if' i as@[cond,then',else'] = gasUnreduced i as $ reduce cond >>= \case
347-
TLiteral (LBool c') _ -> reduce (if c' then then' else else')
348-
t -> evalError' i $ "if: conditional not boolean: " <> pretty t
349+
TLiteral (LBool c') _ -> reduce (if c' then then' else else')
350+
t -> isOffChainForkedError >>= \case
351+
OffChainError -> evalError' i $ "if: conditional not boolean: " <> pretty t
352+
OnChainError -> evalError' i $ "if: conditional not boolean, received value of type: " <> pretty (typeof' t)
353+
349354
if' i as = argsError' i as
350355

351356

@@ -529,8 +534,9 @@ defineNamespaceDef = setTopLevelOnly $ defGasRNative "define-namespace" defineNa
529534
asBool =<< apply (App def' [] i) mkArgs
530535
where
531536
asBool (TLiteral (LBool allow) _) = return allow
532-
asBool t = evalError' fi $
533-
"Unexpected return value from namespace policy: " <> pretty t
537+
asBool t = isOffChainForkedError >>= \case
538+
OffChainError -> evalError' fi $ "Unexpected return value from namespace policy: " <> pretty t
539+
OnChainError -> evalError' fi $ "Unexpected return value from namespace policy, received value of type: " <> pretty (typeof' t)
534540

535541
mkArgs = [toTerm (asString nn),TGuard (_nsAdmin ns) def]
536542

@@ -902,9 +908,12 @@ b = mkTyVar "b" []
902908
c = mkTyVar "c" []
903909

904910
map' :: NativeFun e
905-
map' i as@[tLamToApp -> TApp app _,l] = gasUnreduced i as $ reduce l >>= \l' -> case l' of
911+
map' i as@[tLamToApp -> TApp app _,l] = gasUnreduced i as $ reduce l >>= \case
906912
TList ls _ _ -> (\b' -> TList b' TyAny def) <$> forM ls (apply app . pure)
907-
t -> evalError' i $ "map: expecting list: " <> pretty (abbrev t)
913+
t ->
914+
isOffChainForkedError >>= \case
915+
OffChainError -> evalError' i $ "map: expecting list: " <> pretty (abbrev t)
916+
OnChainError -> evalError' i $ "map: expecting list, received argument of type: " <> pretty (typeof' t)
908917
map' i as = argsError' i as
909918

910919
list :: RNativeFun e
@@ -964,7 +973,10 @@ fold' :: NativeFun e
964973
fold' i as@[tLamToApp -> app@TApp {},initv,l] = gasUnreduced i as $ reduce l >>= \case
965974
TList ls _ _ -> reduce initv >>= \initv' ->
966975
foldM (\r a' -> apply (_tApp app) [r,a']) initv' ls
967-
t -> evalError' i $ "fold: expecting list: " <> pretty (abbrev t)
976+
t ->
977+
isOffChainForkedError >>= \case
978+
OffChainError -> evalError' i $ "fold: expecting list: " <> pretty (abbrev t)
979+
OnChainError -> evalError' i $ "fold: expecting list, received argument of type: " <> pretty (typeof' t)
968980
fold' i as = argsError' i as
969981

970982

@@ -977,7 +989,9 @@ filter' i as@[tLamToApp -> app@TApp {},l] = gasUnreduced i as $ reduce l >>= \ca
977989
_ -> ifExecutionFlagSet FlagDisablePact420
978990
(return False)
979991
(evalError' i $ "filter: expected closure to return bool: " <> pretty app)
980-
t -> evalError' i $ "filter: expecting list: " <> pretty (abbrev t)
992+
t -> isOffChainForkedError >>= \case
993+
OffChainError -> evalError' i $ "filter: expecting list: " <> pretty (abbrev t)
994+
OnChainError -> evalError' i $ "filter: expecting list, received argument of type: " <> pretty (typeof' t)
981995
filter' i as = argsError' i as
982996

983997

@@ -1084,8 +1098,9 @@ bind i as = argsError' i as
10841098
bindObjectLookup :: Term Name -> Eval e (Text -> Maybe (Term Name))
10851099
bindObjectLookup (TObject (Object (ObjectMap o) _ _ _) _) =
10861100
return $ \s -> M.lookup (FieldKey s) o
1087-
bindObjectLookup t = evalError (_tInfo t) $
1088-
"bind: expected object: " <> pretty t
1101+
bindObjectLookup t = isOffChainForkedError >>= \case
1102+
OffChainError -> evalError (_tInfo t) $ "bind: expected object: " <> pretty t
1103+
OnChainError -> evalError (_tInfo t) $ "bind: expected object, received value of type: " <> pretty (typeof' t)
10891104

10901105
typeof'' :: RNativeFun e
10911106
typeof'' _ [t] = return $ tStr $ typeof' t
@@ -1242,7 +1257,9 @@ concat' g i [TList ls _ _] = computeGas' g i (GMakeList $ fromIntegral $ V.lengt
12421257
concatTextList = flip TLiteral def . LString . T.concat
12431258
in fmap concatTextList $ forM ls' $ \case
12441259
TLitString s -> return s
1245-
t -> evalError' i $ "concat: expecting list of strings: " <> pretty t
1260+
t -> isOffChainForkedError >>= \case
1261+
OffChainError -> evalError' i $ "concat: expecting list of strings: " <> pretty t
1262+
OnChainError -> evalError' i $ "concat: expected list of strings, received value of type: " <> pretty (typeof' t)
12461263
concat' _ i as = argsError i as
12471264

12481265
-- | Converts a string to a vector of single character strings
@@ -1375,6 +1392,7 @@ continueNested i as = gasUnreduced i as $ case as of
13751392
TDynamic tref tmem ti -> reduceDynamic tref tmem ti >>= \case
13761393
Right d -> pure d
13771394
Left _ -> evalError' i $ "continue: dynamic reference did not point to Defpact"
1395+
-- Note, pretty on `t` is not dangerous here, as it is not a reduced term.
13781396
_ -> evalError' i $ "continue: argument must be a defpact " <> pretty t
13791397
unTVar = \case
13801398
TVar (Ref d) _ -> unTVar d

src/Pact/Native/Db.hs

+12-5
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,19 @@ foldDB' :: NativeFun e
249249
foldDB' i [tbl, tLamToApp -> TApp qry _, tLamToApp -> TApp consumer _] = do
250250
table <- reduce tbl >>= \case
251251
t@TTable{} -> return t
252-
t -> evalError' i $ "Expected table as first argument to foldDB, got: " <> pretty t
252+
t -> isOffChainForkedError >>= \case
253+
OffChainError -> evalError' i $ "Expected table as first argument to foldDB, got: " <> pretty t
254+
OnChainError -> evalError' i $ "Expected table as first argument to foldDB, got argument of type: " <> pretty (typeof' t)
253255
!g0 <- computeGas (Right i) (GUnreduced [])
254256
!g1 <- computeGas (Right i) GFoldDB
255257
ks <- getKeys table
256258
(!g2, xs) <- foldlM (fdb table) (g0+g1, []) ks
257259
pure (g2, TList (V.fromList (reverse xs)) TyAny def)
258260
where
259261
asBool (TLiteral (LBool satisfies) _) = return satisfies
260-
asBool t = evalError' i $ "Unexpected return value from fold-db query condition " <> pretty t
262+
asBool t = isOffChainForkedError >>= \case
263+
OffChainError -> evalError' i $ "Unexpected return value from fold-db query condition " <> pretty t
264+
OnChainError -> evalError' i $ "Unexpected return value from fold-db query condition, received value of type: " <> pretty (typeof' t)
261265
getKeys table = do
262266
guardTable i table GtKeys
263267
keys (_faInfo i) (userTable table)
@@ -336,8 +340,9 @@ select' i _ cols' app@TApp{} tbl@TTable{} = do
336340
Nothing -> return (obj:rs)
337341
Just cols -> (:rs) <$> columnsToObject' tblTy cols row
338342
| otherwise -> return rs
339-
t -> evalError (_tInfo app) $ "select: filter returned non-boolean value: "
340-
<> pretty t
343+
t -> isOffChainForkedError >>= \case
344+
OffChainError -> evalError (_tInfo app) $ "select: filter returned non-boolean value: " <> pretty t
345+
OnChainError -> evalError (_tInfo app) $ "select: filter returned non-boolean value: " <> pretty (typeof' t)
341346
select' i as _ _ _ = argsError' i as
342347

343348

@@ -472,7 +477,9 @@ guardTable i TTable {..} dbop =
472477
_ | localBypassEnabled -> return ()
473478
| otherwise -> notBypassed
474479

475-
guardTable i t _ = evalError' i $ "Internal error: guardTable called with non-table term: " <> pretty t
480+
guardTable i t _ = isOffChainForkedError >>= \case
481+
OffChainError -> evalError' i $ "Internal error: guardTable called with non-table term: " <> pretty t
482+
OnChainError -> evalError' i $ "Internal error: guardTable called with non-table term: " <> pretty (typeof' t)
476483

477484
enforceBlessedHashes :: FunApp -> ModuleName -> ModuleHash -> Eval e ()
478485
enforceBlessedHashes i mn h = getModule i mn >>= \m -> case (_mdModule m) of

src/Pact/Repl/Lib.hs

+16-4
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ replDefs = ("Repl",
270270
[LitExample "(env-dynref fungible-v2 coin)"]
271271
("Substitute module IMPL in any dynamic usages of IFACE in typechecking and analysis. " <>
272272
"With no arguments, remove all substitutions.")
273+
,defZRNative "env-simulate-onchain" envSimulateOnChain
274+
(funType tTyString [("on-chain", tTyBool)])
275+
[LitExample "(env-simulate-onchain true)"]
276+
"Set a flag to simulate on-chain behavior that differs from the repl, in particular for observing things like errors and stack traces."
273277
])
274278
where
275279
json = mkTyVar "a" [tTyInteger,tTyString,tTyTime,tTyDecimal,tTyBool,
@@ -545,8 +549,8 @@ testCatch i doc expr errMsg cont = catchesPactError expr >>= \r -> case r of
545549
expect :: ZNativeFun LibState
546550
expect i as@[_,b',c'] = do
547551
doc <- testDoc i as
548-
testCatch i doc (reduce c') "evaluation of actual failed" $ \c ->
549-
testCatch i doc (reduce b') "evaluation of expected failed" $ \b ->
552+
testCatch i doc (reduceEnscoped c') "evaluation of actual failed" $ \c ->
553+
testCatch i doc (reduceEnscoped b') "evaluation of expected failed" $ \b ->
550554
if b `termEq` c
551555
then testSuccess doc "Expect"
552556
else testFailure i doc $
@@ -565,7 +569,7 @@ expectFail i as = case as of
565569
tsuccess msg = testSuccess msg "Expect failure"
566570
go errM expr = do
567571
msg' <- testDoc i as
568-
r <- catch (Right <$> reduce expr) (\(e :: SomeException) -> return $ Left (show e))
572+
r <- catch (Right <$> reduceEnscoped expr) (\(e :: SomeException) -> return $ Left (show e))
569573
case r of
570574
Right v -> testFailure i msg' $ "expected failure, got result = " <> pretty v
571575
Left e -> case errM of
@@ -579,7 +583,7 @@ expectFail i as = case as of
579583
expectThat :: ZNativeFun LibState
580584
expectThat i as@[_,tLamToApp -> TApp pred' predi,expr'] = do
581585
doc <- testDoc i as
582-
testCatch i doc (reduce expr') "evaluation of expression failed" $ \v ->
586+
testCatch i doc (reduceEnscoped expr') "evaluation of expression failed" $ \v ->
583587
testCatch i doc (apply pred' [v]) "evaluation of predicate failed" $ \p -> case p of
584588
TLitBool b
585589
| b -> testSuccess doc $ "Expect-that"
@@ -867,3 +871,11 @@ withEnv _ [exec] = do
867871
_ -> (ls,Endo id)
868872
local (appEndo updates) $ reduce exec
869873
withEnv i as = argsError' i as
874+
875+
envSimulateOnChain :: RNativeFun LibState
876+
envSimulateOnChain _i [TLiteral (LBool simulateOnChain) _] = do
877+
-- Note: Simulating on-chain means we are _not_ `inRepl`
878+
setenv eeInRepl (not simulateOnChain)
879+
let ppInRepl = if simulateOnChain then "true" else "false"
880+
return $ tStr $ "Set on-chain simulation execution mode to: " <> ppInRepl
881+
envSimulateOnChain i as = argsError i as

src/Pact/Types/Capability.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ data Capabilities = Capabilities
181181
-- initialized from signature set.
182182
, _capModuleAdmin :: (Set ModuleName)
183183
-- ^ Set of module admin capabilities.
184-
, _capAutonomous :: (Set UserCapability)
184+
, _capAutonomous :: Set UserCapability
185185
}
186186
deriving (Eq,Show,Generic)
187187

src/Pact/Types/Runtime.hs

+45-7
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ module Pact.Types.Runtime
5555
module Pact.Types.ChainMeta,
5656
module Pact.Types.PactError,
5757
liftIO,
58-
eAdvise
58+
eAdvise,
59+
isOffChainForkedError,
60+
OnChainErrorState(..)
5961
) where
6062

6163

@@ -77,6 +79,7 @@ import Data.Text (Text,pack)
7779
import Data.Set(Set)
7880
import GHC.Generics (Generic)
7981

82+
import Pact.Types.Term
8083
import Pact.Types.Capability
8184
import Pact.Types.ChainMeta
8285
import Pact.Types.Continuation
@@ -180,8 +183,10 @@ data ExecutionFlag
180183
| FlagDisableNewTrans
181184
-- | Disable Pact 4.5 Features
182185
| FlagDisablePact45
183-
-- | Disable Pact 4.6 Features
186+
-- | Disable Pact 4.6 Features
184187
| FlagDisablePact46
188+
-- | Disable Pact 4.7 Features
189+
| FlagDisablePact47
185190
deriving (Eq,Ord,Show,Enum,Bounded)
186191

187192
-- | Flag string representation
@@ -434,17 +439,44 @@ throwArgsError FunApp {..} args s = throwErr ArgsError _faInfo $
434439
pretty s <> ", received " <> bracketsSep (map pretty args) <> " for " <>
435440
prettyFunTypes _faTypes
436441

442+
throwOnChainArgsError :: Pretty n => FunApp -> [Term n] -> Eval e a
443+
throwOnChainArgsError FunApp{..} args = throwErr ArgsError _faInfo $
444+
"Invalid arguments in call to"
445+
<+> pretty _faName
446+
<> ", received arguments of type "
447+
<> bracketsSep (map (pretty . typeof') args) <> ", expected "
448+
<> prettyFunTypes _faTypes
449+
437450
throwErr :: PactErrorType -> Info -> Doc -> Eval e a
438-
throwErr ctor i err = get >>= \s -> throwM (PactError ctor i (_evalCallStack s) err)
451+
throwErr ctor i err = do
452+
s <- use evalCallStack
453+
offChainOrPreFork <- isOffChainForkedError'
454+
throwM (PactError ctor i (if offChainOrPreFork then s else []) err)
439455

440456
evalError :: Info -> Doc -> Eval e a
441-
evalError i = throwErr EvalError i
457+
evalError = throwErr EvalError
442458

443459
evalError' :: HasInfo i => i -> Doc -> Eval e a
444460
evalError' = evalError . getInfo
445461

462+
data OnChainErrorState
463+
= OnChainError
464+
| OffChainError
465+
deriving (Eq, Show)
466+
467+
-- | Function to determine whether we are either pre-errors fork
468+
-- or in a repl environment.
469+
isOffChainForkedError :: Eval e OnChainErrorState
470+
isOffChainForkedError = isOffChainForkedError' <&> \p -> if p then OffChainError else OnChainError
471+
472+
isOffChainForkedError' :: Eval e Bool
473+
isOffChainForkedError' =
474+
isExecutionFlagSet FlagDisablePact47 >>= \case
475+
True -> pure True
476+
False -> view eeInRepl
477+
446478
failTx :: Info -> Doc -> Eval e a
447-
failTx i = throwErr TxFailure i
479+
failTx = throwErr TxFailure
448480

449481
failTx' :: HasInfo i => i -> Doc -> Eval e a
450482
failTx' = failTx . getInfo
@@ -461,10 +493,16 @@ throwEitherText typ i d = either (\e -> throwErr typ i (d <> ":" <> pretty e)) r
461493

462494

463495
argsError :: FunApp -> [Term Name] -> Eval e a
464-
argsError i as = throwArgsError i as "Invalid arguments"
496+
argsError i as =
497+
isOffChainForkedError >>= \case
498+
OffChainError -> throwArgsError i as "Invalid arguments"
499+
OnChainError -> throwOnChainArgsError i as
465500

466501
argsError' :: FunApp -> [Term Ref] -> Eval e a
467-
argsError' i as = throwArgsError i (map (toTerm.abbrev) as) "Invalid arguments"
502+
argsError' i as =
503+
isOffChainForkedError >>= \case
504+
OffChainError -> throwArgsError i (map (toTerm.abbrev) as) "Invalid arguments"
505+
OnChainError -> throwOnChainArgsError i as
468506

469507
eAdvise :: Info -> AdviceContext r -> Eval e (r -> Eval e ())
470508
eAdvise i m = view eeAdvice >>= \adv -> advise i adv m

0 commit comments

Comments
 (0)