Skip to content

Commit

Permalink
feat: optionally do not evaluate under binders (except lets) (#1055)
Browse files Browse the repository at this point in the history
  • Loading branch information
brprice committed Sep 18, 2023
2 parents 23637df + 8a6a846 commit 1ab647e
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 51 deletions.
12 changes: 8 additions & 4 deletions primer-api/src/Primer/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ import Primer.Def (
defAST,
)
import Primer.Def qualified as Def
import Primer.Eval (NormalOrderOptions (StopAtBinders))
import Primer.Eval.Redex (Dir (Chk), EvalLog)
import Primer.EvalFull (TerminationBound)
import Primer.JSON (
Expand Down Expand Up @@ -412,7 +413,7 @@ data APILog
| GenerateNames (ReqResp (SessionId, ((GVarName, ID), Either (Maybe (Type' ())) (Maybe (Kind' ())))) (Either ProgError [Name.Name]))
| EvalStep (ReqResp (SessionId, EvalReq) (Either ProgError EvalResp))
| EvalFull (ReqResp (SessionId, EvalFullReq) (Either ProgError App.EvalFullResp))
| EvalFull' (ReqResp (SessionId, Maybe TerminationBound, GVarName) EvalFullResp)
| EvalFull' (ReqResp (SessionId, Maybe TerminationBound, Maybe NormalOrderOptions, GVarName) EvalFullResp)
| FlushSessions (ReqResp () ())
| CreateDef (ReqResp (SessionId, ModuleName, Maybe Text) Prog)
| CreateTypeDef (ReqResp (SessionId, TyConName, [ValConName]) Prog)
Expand Down Expand Up @@ -1117,16 +1118,18 @@ evalFull' ::
(MonadIO m, MonadThrow m, MonadAPILog l m, ConvertLogMessage EvalLog l) =>
SessionId ->
Maybe TerminationBound ->
Maybe NormalOrderOptions ->
GVarName ->
PrimerM m EvalFullResp
evalFull' = curry3 $ logAPI (noError EvalFull') $ \(sid, lim, d) ->
noErr <$> liftQueryAppM (q lim d) sid
evalFull' = curry4 $ logAPI (noError EvalFull') $ \(sid, lim, closed, d) ->
noErr <$> liftQueryAppM (q lim closed d) sid
where
q ::
Maybe TerminationBound ->
Maybe NormalOrderOptions ->
GVarName ->
QueryAppM (PureLog (WithSeverity l)) Void EvalFullResp
q lim d = do
q lim closed d = do
-- We don't care about uniqueness of this ID, and we do not want to
-- disturb any FreshID state, since that could break undo/redo.
-- The reason we don't care about uniqueness is that this node will never
Expand All @@ -1139,6 +1142,7 @@ evalFull' = curry3 $ logAPI (noError EvalFull') $ \(sid, lim, d) ->
{ evalFullReqExpr = e
, evalFullCxtDir = Chk
, evalFullMaxSteps = fromMaybe 10 lim
, evalFullOptions = fromMaybe StopAtBinders closed
}
pure $ case x of
App.EvalFullRespTimedOut e' -> EvalFullRespTimedOut $ viewTreeExpr e'
Expand Down
3 changes: 2 additions & 1 deletion primer-api/test/Tests/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import Primer.Database (
fromSessionName,
)
import Primer.Def (astDefExpr, astDefType, defAST)
import Primer.Eval (NormalOrderOptions (UnderBinders))
import Primer.Examples (
comprehensive,
even3App,
Expand Down Expand Up @@ -462,7 +463,7 @@ test_eval_undo =
sid <- newSession $ NewSessionReq "a new session" True
let scope = mkSimpleModuleName "Main"
step "eval"
void $ evalFull' sid (Just 100) $ qualifyName scope "main"
void $ evalFull' sid (Just 100) (Just UnderBinders) $ qualifyName scope "main"
step "insert λ"
let getMain = do
p <- getProgram sid
Expand Down
6 changes: 4 additions & 2 deletions primer-benchmark/src/Benchmarks.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Primer.App (
)
import Primer.App.Utils (forgetProgTypecache)
import Primer.Eval (
NormalOrderOptions (UnderBinders),
RunRedexOptions (RunRedexOptions, pushAndElide),
ViewRedexOptions (ViewRedexOptions, aggressiveElision, groupedLets),
)
Expand Down Expand Up @@ -102,16 +103,17 @@ benchmarks =
]
]
where
evalOptionsN = UnderBinders
evalOptionsV = ViewRedexOptions{groupedLets = True, aggressiveElision = True}
evalOptionsR = RunRedexOptions{pushAndElide = True}
evalTestMPureLogs e maxEvals =
evalTestM (maxID e) $
runPureLogT $
evalFull @EvalLog evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
evalFull @EvalLog evalOptionsN evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
evalTestMDiscardLogs e maxEvals =
evalTestM (maxID e) $
runDiscardLogT $
evalFull @EvalLog evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
evalFull @EvalLog evalOptionsN evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)

benchExpected f g e n b = EnvBench e n $ \e' ->
NF
Expand Down
6 changes: 6 additions & 0 deletions primer-service/src/Primer/OpenAPI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import Primer.Database (
Session,
SessionName,
)
import Primer.Eval (NormalOrderOptions)
import Primer.JSON (CustomJSON (CustomJSON), PrimerJSON)
import Primer.Name (Name)
import Servant.API (FromHttpApiData (parseQueryParam), ToHttpApiData (toQueryParam))
Expand Down Expand Up @@ -198,6 +199,11 @@ instance FromHttpApiData Level where
parseQueryParam = parseQueryParamRead "level"
instance ToHttpApiData Level where
toQueryParam = show
deriving anyclass instance ToParamSchema NormalOrderOptions
instance FromHttpApiData NormalOrderOptions where
parseQueryParam = parseQueryParamRead "NormalOrderOptions"
instance ToHttpApiData NormalOrderOptions where
toQueryParam = show
parseQueryParamRead :: Read a => Text -> Text -> Either Text a
parseQueryParamRead m t = maybeToEither ("unknown " <> m <> ": " <> t) $ readMaybe t

Expand Down
2 changes: 2 additions & 0 deletions primer-service/src/Primer/Servant/OpenAPI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Primer.Core (GVarName, ModuleName)
import Primer.Database (
SessionId,
)
import Primer.Eval (NormalOrderOptions)
import Primer.Finite (Finite)
import Primer.JSON (CustomJSON (CustomJSON), FromJSON, PrimerJSON, ToJSON)
import Primer.OpenAPI ()
Expand Down Expand Up @@ -124,6 +125,7 @@ data SessionAPI mode = SessionAPI
:> Summary "Evaluate the named definition to normal form (or time out)"
:> OperationId "eval-full"
:> QueryParam "stepLimit" (Finite 0 EvalFullStepLimit)
:> QueryParam "closed" NormalOrderOptions
:> ReqBody '[JSON] GVarName
:> Post '[JSON] EvalFullResp
, undo ::
Expand Down
14 changes: 13 additions & 1 deletion primer-service/test/outputs/OpenAPI/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,18 @@
"minimum": 0,
"type": "integer"
}
},
{
"in": "query",
"name": "closed",
"required": false,
"schema": {
"enum": [
"UnderBinders",
"StopAtBinders"
],
"type": "string"
}
}
],
"requestBody": {
Expand All @@ -1509,7 +1521,7 @@
"description": ""
},
"400": {
"description": "Invalid `body` or `stepLimit`"
"description": "Invalid `body` or `closed` or `stepLimit`"
},
"404": {
"description": "`sessionId` not found"
Expand Down
5 changes: 3 additions & 2 deletions primer/src/Primer/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ data EvalFullReq = EvalFullReq
{ evalFullReqExpr :: Expr
, evalFullCxtDir :: Dir -- is this expression in a syn/chk context, so we can tell if is an embedding.
, evalFullMaxSteps :: TerminationBound
, evalFullOptions :: Eval.NormalOrderOptions
}
deriving stock (Eq, Show, Read, Generic)
deriving (FromJSON, ToJSON) via PrimerJSON EvalFullReq
Expand Down Expand Up @@ -595,12 +596,12 @@ handleEvalFullRequest ::
(MonadQueryApp m e, MonadLog (WithSeverity l) m, ConvertLogMessage EvalLog l) =>
EvalFullReq ->
m EvalFullResp
handleEvalFullRequest (EvalFullReq{evalFullReqExpr, evalFullCxtDir, evalFullMaxSteps}) = do
handleEvalFullRequest (EvalFullReq{evalFullReqExpr, evalFullCxtDir, evalFullMaxSteps, evalFullOptions}) = do
app <- ask
let prog = appProg app
let optsV = ViewRedexOptions{groupedLets = True, aggressiveElision = True}
let optsR = RunRedexOptions{pushAndElide = True}
result <- runFreshM app $ evalFull optsV optsR (allTypes prog) (allDefs prog) evalFullMaxSteps evalFullCxtDir evalFullReqExpr
result <- runFreshM app $ evalFull evalFullOptions optsV optsR (allTypes prog) (allDefs prog) evalFullMaxSteps evalFullCxtDir evalFullReqExpr
pure $ case result of
Left (TimedOut e) -> EvalFullRespTimedOut e
Right nf -> EvalFullRespNormal nf
Expand Down
9 changes: 9 additions & 0 deletions primer/src/Primer/Eval.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Primer.Eval (
redexes,
RunRedexOptions (..),
ViewRedexOptions (..),
NormalOrderOptions (..),
EvalLog (..),
EvalError (..),
EvalDetail (..),
Expand Down Expand Up @@ -57,6 +58,7 @@ import Primer.Eval.Detail (
import Primer.Eval.EvalError (EvalError (..))
import Primer.Eval.NormalOrder (
FMExpr (FMExpr, expr, ty),
NormalOrderOptions (..),
foldMapExpr,
singletonCxt,
)
Expand Down Expand Up @@ -114,6 +116,7 @@ step tydefs globals expr d i = runExceptT $ do
findNodeByID :: ID -> Dir -> Expr -> Maybe (Cxt, Either (Dir, ExprZ) TypeZ)
findNodeByID i =
foldMapExpr
UnderBinders
FMExpr
{ expr = \ez d c -> if getID ez == i then Just (c, Left (d, ez)) else Nothing
, ty = \tz c -> if getID tz == i then Just (c, Right tz) else Nothing
Expand All @@ -132,6 +135,11 @@ evalOpts =
-- We assume that the expression is well scoped. There are no
-- guarantees about whether we will claim that an ill-sorted variable
-- is inlinable, e.g. @lettype a = _ in case a of ...@.
--
-- NB: we return /all/ redexes, even those under binders. We ignore any
-- `NormalOrderOptions`. This means that more redexes may be returned than an
-- EvalFull would actually reduce (depending on the NormalOrderOptions given to
-- EvalFull).
redexes ::
forall l m.
(MonadLog (WithSeverity l) m, ConvertLogMessage EvalLog l) =>
Expand All @@ -143,6 +151,7 @@ redexes ::
redexes tydefs globals =
(ListT.toList .)
. foldMapExpr
UnderBinders
FMExpr
{ expr = \ez d -> liftMaybeT . runReaderT (getID ez <$ viewRedex evalOpts tydefs globals d (target ez))
, ty = \tz -> runReader (whenJust (getID tz) <$> viewRedexType evalOpts (target tz))
Expand Down
75 changes: 60 additions & 15 deletions primer/src/Primer/Eval/NormalOrder.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Primer.Eval.NormalOrder (
findRedex,
foldMapExpr,
FMExpr (..),
NormalOrderOptions (..),
-- Exported for testing
singletonCxt,
) where
Expand All @@ -21,11 +22,14 @@ import Primer.Core (
App,
Case,
Hole,
LAM,
Lam,
Let,
LetType,
Letrec
),
Type' (
TForall,
TLet
),
)
Expand All @@ -43,6 +47,7 @@ import Primer.Eval.Redex (
viewRedex,
viewRedexType,
)
import Primer.JSON (CustomJSON (CustomJSON), FromJSON, PrimerJSON, ToJSON)
import Primer.Log (ConvertLogMessage)
import Primer.TypeDef (
TypeDefMap,
Expand Down Expand Up @@ -81,7 +86,7 @@ data ViewLet = ViewLet
-- ^ any non-body term children (i.e. rhs of the binding)
}
viewLet :: (Dir, ExprZ) -> Maybe ViewLet
viewLet dez@(_, ez) = case (target ez, exprChildren dez) of
viewLet dez@(_, ez) = case (target ez, exprChildren UnderBinders dez) of
(Let _ x e _b, [ez', bz]) -> Just (ViewLet (LetBind x e) bz [] [ez'])
(Letrec _ x e ty _b, [ez', bz]) -> Just (ViewLet (LetrecBind x e ty) bz tz [ez'])
(LetType _ a ty _b, [bz]) -> bz `seq` Just (ViewLet (LetTyBind $ LetTypeBind a ty) bz tz [])
Expand All @@ -90,6 +95,12 @@ viewLet dez@(_, ez) = case (target ez, exprChildren dez) of
tz :: [TypeZ]
tz = maybeToList $ focusType ez

data NormalOrderOptions
= UnderBinders
| StopAtBinders
deriving stock (Eq, Show, Read, Generic)
deriving (FromJSON, ToJSON) via PrimerJSON NormalOrderOptions

-- | This is similar to 'foldMap', with a few differences:
-- - 'Expr' is not foldable
-- - We target every subexpression and also every (nested) subtype (e.g. in an annotation)
Expand All @@ -107,8 +118,15 @@ viewLet dez@(_, ez) = case (target ez, exprChildren dez) of
-- However, we may reduce type children to normal form more eagerly than necessary,
-- both reducing type annotations more than needed, and reducing type applications when not needed.
-- Since computation in types is strongly normalising, this will not cause us to fail to find any normal forms.
foldMapExpr :: forall f a. MonadPlus f => FMExpr (f a) -> Dir -> Expr -> f a
foldMapExpr extract topDir = go mempty . (topDir,) . focus
--
-- We can optionally stop when we find a binder (e.g. to implement closed
-- evaluation -- do not compute under binders), although for consistency we
-- treat all case branches as being binders, even those that do not actually
-- bind a variable. Note that (for the case where we reduce a stack of @let@s
-- one-by-one inside-out) we need to go under let bindings, but stop when we
-- find a non-@let@.
foldMapExpr :: forall f a. MonadPlus f => NormalOrderOptions -> FMExpr (f a) -> Dir -> Expr -> f a
foldMapExpr opts extract topDir = go mempty . (topDir,) . focus
where
go :: Cxt -> (Dir, ExprZ) -> f a
go lets dez@(d, ez) =
Expand All @@ -125,17 +143,20 @@ foldMapExpr extract topDir = go mempty . (topDir,) . focus
-- its type annotation, we can handle them all uniformly
-- Since this node is not a let, the context is reset
_ ->
msum $
(goType mempty =<< focusType' ez)
: map (go mempty) (exprChildren dez)
case (opts, lets) of
(StopAtBinders, Cxt (_ : _)) -> mzero
_ ->
msum $
(goType mempty =<< focusType' ez) -- NB: no binders in term scope over a type child
: map (go mempty) (exprChildren opts dez)
goType :: Cxt -> TypeZ -> f a
goType lets tz =
extract.ty tz lets
<|> case target tz of
TLet _ a t _body
-- Prefer to compute inside the body of a let, but otherwise compute in the binding
| [tz', bz] <- typeChildren tz -> goType (cxtAddLet (LetTyBind $ LetTypeBind a t) lets) bz <|> goType mempty tz'
_ -> msum $ map (goType mempty) $ typeChildren tz
| [tz', bz] <- typeChildren UnderBinders tz -> goType (cxtAddLet (LetTyBind $ LetTypeBind a t) lets) bz <|> goType mempty tz'
_ -> msum $ map (goType mempty) $ typeChildren opts tz

data FMExpr m = FMExpr
{ expr :: ExprZ -> Dir -> Cxt -> m
Expand All @@ -149,17 +170,19 @@ focusType' = maybe empty pure . focusType
findRedex ::
forall l m.
(MonadLog (WithSeverity l) m, ConvertLogMessage EvalLog l) =>
NormalOrderOptions ->
ViewRedexOptions ->
TypeDefMap ->
DefMap ->
Dir ->
Expr ->
MaybeT m RedexWithContext
findRedex opts tydefs globals =
findRedex optsN optsV tydefs globals =
foldMapExpr
optsN
( FMExpr
{ expr = \ez d -> runReaderT (RExpr ez <$> viewRedex opts tydefs globals d (target ez))
, ty = \tz -> hoistMaybe . runReader (RType tz <<$>> viewRedexType opts (target tz))
{ expr = \ez d -> runReaderT (RExpr ez <$> viewRedex optsV tydefs globals d (target ez))
, ty = \tz -> hoistMaybe . runReader (RType tz <<$>> viewRedexType optsV (target tz))
}
)

Expand All @@ -168,8 +191,8 @@ children' z = case down z of
Nothing -> mempty
Just z' -> z' : unfoldr (fmap (\x -> (x, x)) . right) z'

exprChildren :: (Dir, ExprZ) -> [(Dir, ExprZ)]
exprChildren (d, ez) =
exprChildren' :: (Dir, ExprZ) -> [(Dir, ExprZ)]
exprChildren' (d, ez) =
children' ez <&> \c -> do
let d' = case target ez of
App _ f _ | f == target c -> Syn
Expand All @@ -188,8 +211,30 @@ exprChildren (d, ez) =
_ -> Chk
(d', c)

typeChildren :: TypeZ -> [TypeZ]
typeChildren = children'
-- Extract the children of the current focus, except those under an binder.
-- This is used to restrict our evaluation to "closed evaluation".
-- NB: for consistency we skip all case branches, not just those that bind a variable.
exprChildrenClosed :: (Dir, ExprZ) -> [(Dir, ExprZ)]
exprChildrenClosed (d, ez) = case target ez of
Lam{} -> []
LAM{} -> []
Let{} -> take 1 $ exprChildren' (d, ez) -- just the binding
LetType{} -> []
Letrec{} -> []
Case{} -> take 1 $ exprChildren' (d, ez) -- just the scrutinee
_ -> exprChildren' (d, ez)

exprChildren :: NormalOrderOptions -> (Dir, ExprZ) -> [(Dir, ExprZ)]
exprChildren = \case
UnderBinders -> exprChildren'
StopAtBinders -> exprChildrenClosed

typeChildren :: NormalOrderOptions -> TypeZ -> [TypeZ]
typeChildren = \case
UnderBinders -> children'
StopAtBinders -> \tz -> case target tz of
TForall{} -> []
_ -> children' tz

singletonCxt :: LetBinding -> Cxt
singletonCxt l = Cxt [l]
Loading

1 comment on commit 1ab647e

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Primer benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: 1ab647e Previous: 23637df Ratio
evalTestM/pure logs/mapEven 1: outlier variance 0.12128739751917161 outlier variance 0.02498356344510181 outlier variance 4.85
typecheck/mapOddPrim 100: outlier variance 0.7009450870362847 outlier variance 0.1546899745389029 outlier variance 4.53

This comment was automatically generated by workflow using github-action-benchmark.

CC: @dhess

Please sign in to comment.