Skip to content

Commit

Permalink
feat: interpreter (faster evaluator-to-normal-form) (#1187)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhess authored Dec 5, 2023
2 parents 9865b85 + c545482 commit 6b8368f
Show file tree
Hide file tree
Showing 20 changed files with 1,790 additions and 68 deletions.
2 changes: 1 addition & 1 deletion primer-api/src/Primer/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ import Primer.Def (
import Primer.Def qualified as Def
import Primer.Eval (NormalOrderOptions (StopAtBinders))
import Primer.Eval.Redex (Dir (Chk), EvalLog)
import Primer.EvalFull (TerminationBound)
import Primer.EvalFullStep (TerminationBound)
import Primer.JSON (
CustomJSON (..),
FromJSON,
Expand Down
47 changes: 32 additions & 15 deletions primer-benchmark/src/Benchmarks.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ import Primer.App (
tcWholeProgWithImports,
)
import Primer.App.Utils (forgetProgTypecache)
import Primer.Core.Utils (forgetMetadata)
import Primer.Eval (
NormalOrderOptions (UnderBinders),
RunRedexOptions (RunRedexOptions, pushAndElide),
ViewRedexOptions (ViewRedexOptions, aggressiveElision, avoidShadowing, groupedLets),
)
import Primer.EvalFull (
import Primer.EvalFullInterp qualified as EFInterp
import Primer.EvalFullStep (
Dir (Syn),
EvalLog,
evalFull,
)
import Primer.EvalFullStep qualified as EFStep
import Primer.Examples (
mapOddPrimProg,
mapOddProg,
Expand Down Expand Up @@ -79,17 +80,23 @@ benchmarks =
"evalTestM"
[ Group
"pure logs"
[ benchExpectedPureLogs (mapEvenEnv 1) "mapEven 1" 100
, benchExpectedPureLogs (mapEvenEnv 10) "mapEven 10" 1000
[ benchExpectedPureLogsStep (mapEvenEnv 1) "mapEven 1" 100
, benchExpectedPureLogsStep (mapEvenEnv 10) "mapEven 10" 1000
-- This benchmark is too slow to be practical for CI.
-- , benchExpectedPureLogs (mapEvenEnv 100) "mapEven 100" 10000
-- , benchExpectedPureLogsStep (mapEvenEnv 100) "mapEven 100" 10000
]
, Group
"discard logs"
[ benchExpectedDiscardLogs (mapEvenEnv 1) "mapEven 1" 100
, benchExpectedDiscardLogs (mapEvenEnv 10) "mapEven 10" 1000
[ benchExpectedDiscardLogsStep (mapEvenEnv 1) "mapEven 1" 100
, benchExpectedDiscardLogsStep (mapEvenEnv 10) "mapEven 10" 1000
-- This benchmark is too slow to be practical for CI.
-- , benchExpectedDiscardLogs (mapEvenEnv 100) "mapEven 100" 10000
-- , benchExpectedDiscardLogsStep (mapEvenEnv 100) "mapEven 100" 10000
]
, Group
"interp (has no logs)"
[ benchExpectedInterp (mapEvenEnv 1) "mapEven 1" Syn
, benchExpectedInterp (mapEvenEnv 10) "mapEven 10" Syn
, benchExpectedInterp (mapEvenEnv 100) "mapEven 100" Syn
]
]
, Group
Expand All @@ -106,23 +113,33 @@ benchmarks =
evalOptionsN = UnderBinders
evalOptionsV = ViewRedexOptions{groupedLets = True, aggressiveElision = True, avoidShadowing = False}
evalOptionsR = RunRedexOptions{pushAndElide = True}
evalTestMPureLogs e maxEvals =
evalTestMPureLogsStep e maxEvals =
evalTestM (maxID e)
$ runPureLogT
$ evalFull @EvalLog evalOptionsN evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
evalTestMDiscardLogs e maxEvals =
$ EFStep.evalFull @EFStep.EvalLog evalOptionsN evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
evalTestMDiscardLogsStep e maxEvals =
evalTestM (maxID e)
$ runDiscardLogT
$ evalFull @EvalLog evalOptionsN evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
$ EFStep.evalFull @EFStep.EvalLog evalOptionsN evalOptionsV evalOptionsR builtinTypes (defMap e) maxEvals Syn (expr e)
evalTestMInterp e d =
EFInterp.interp' builtinTypes (EFInterp.mkGlobalEnv $ defMap e) d (forgetMetadata $ expr e)

benchExpected f g e n b = EnvBench e n $ \e' ->
NF
(f e')
b
(pure $ (@?= Right (zeroIDs $ expectedResult e')) . fmap zeroIDs . g)

benchExpectedPureLogs = benchExpected evalTestMPureLogs fst
benchExpectedDiscardLogs = benchExpected evalTestMDiscardLogs identity
-- as benchExpected, but 'interp' works on un-metadata'd stuff
benchExpected' f e n b = EnvBench e n $ \e' ->
NF
(f e')
b
(pure (@?= (forgetMetadata $ expectedResult e')))

benchExpectedPureLogsStep = benchExpected evalTestMPureLogsStep fst
benchExpectedDiscardLogsStep = benchExpected evalTestMDiscardLogsStep identity
benchExpectedInterp = benchExpected' evalTestMInterp

tcTest id = evalTestM id . runExceptT @TypeError . tcWholeProgWithImports

Expand Down
27 changes: 10 additions & 17 deletions primer/gen/Primer/Gen/Core/Typed.hs
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ import Primer.Core (
qualifyName,
)
import Primer.Core.DSL (S)
import Primer.Core.Utils (freeVarsTy)
import Primer.Core.Utils (freeVarsTy, freshen)
import Primer.Gen.Core.Raw (genLVarName, genModuleName, genName, genTyVarName)
import Primer.Module (Module (..))
import Primer.Name (Name, NameCounter, freshName, unName, unsafeMkName)
import Primer.Name (Name, NameCounter, freshName)
import Primer.Refine (Inst (InstAPP, InstApp, InstUnconstrainedAPP), refine)
import Primer.Subst (substTy, substTySimul)
import Primer.Test.TestM (
TestM,
evalTestM,
TestT,
evalTestT,
isolateTestM,
)
import Primer.TypeDef (
Expand Down Expand Up @@ -130,14 +130,15 @@ type TypeG = Type' () ()

type ExprG = Expr' () () ()

newtype WT a = WT {unWT :: ReaderT Cxt TestM a}
newtype WT a = WT {unWT :: ReaderT Cxt (TestT IO) a}
deriving newtype
( Functor
, Applicative
, Monad
, MonadReader Cxt
, MonadFresh NameCounter
, MonadFresh ID
, MonadIO
)

-- | Run an action and ignore any effect on the fresh name/id state
Expand Down Expand Up @@ -198,24 +199,16 @@ freshTyConNameForCxt = qualifyName <$> genModuleName <*> freshNameForCxt
-- the original type variable "foo" by our new term variable "foo".
genLVarNameAvoiding :: [TypeG] -> GenT WT LVarName
genLVarNameAvoiding ty =
(\vs -> freshen (foldMap' freeVarsTy ty <> foldMap' freeVarsTy vs) 0)
(\vs -> freshen (foldMap' (S.map unLocalName . freeVarsTy) ty <> foldMap' (S.map unLocalName . freeVarsTy) vs))
<$> asks localTmVars
<*> genLVarName

genTyVarNameAvoiding :: TypeG -> GenT WT TyVarName
genTyVarNameAvoiding ty =
(\vs -> freshen (freeVarsTy ty <> foldMap' freeVarsTy vs) 0)
(\vs -> freshen (S.map unLocalName (freeVarsTy ty) <> foldMap' (S.map unLocalName . freeVarsTy) vs))
<$> asks localTmVars
<*> genTyVarName

freshen :: Set (LocalName k') -> Int -> LocalName k -> LocalName k
freshen fvs i n =
let suffix = if i > 0 then "_" <> show i else ""
m = LocalName $ unsafeMkName $ unName (unLocalName n) <> suffix
in if m `elem` fvs
then freshen fvs (i + 1) n
else m

-- genSyns T with cxt Γ should generate (e,S) st Γ |- e ∈ S and S ~ T (i.e. same up to holes and alpha)
genSyns :: HasCallStack => TypeG -> GenT WT (ExprG, TypeG)
genSyns ty = do
Expand Down Expand Up @@ -701,8 +694,8 @@ genInt =
where
intBound = fromIntegral (maxBound :: Word64) -- arbitrary

hoist' :: Applicative f => Cxt -> WT a -> f a
hoist' cxt = pure . evalTestM 0 . flip runReaderT cxt . unWT
hoist' :: Cxt -> WT a -> IO a
hoist' cxt = evalTestT 0 . flip runReaderT cxt . unWT

-- | Convert a @PropertyT WT ()@ into a @Property@, which Hedgehog can test.
-- It is recommended to do more than default number of tests when using this module.
Expand Down
6 changes: 4 additions & 2 deletions primer/primer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ library
Primer.Def.Utils
Primer.Eval
Primer.Eval.Redex
Primer.EvalFull
Primer.EvalFullInterp
Primer.EvalFullStep
Primer.Examples
Primer.JSON
Primer.Log
Expand Down Expand Up @@ -216,7 +217,8 @@ test-suite primer-test
Tests.Database
Tests.Eval
Tests.Eval.Utils
Tests.EvalFull
Tests.EvalFullInterp
Tests.EvalFullStep
Tests.Examples
Tests.FreeVars
Tests.Gen.App
Expand Down
2 changes: 1 addition & 1 deletion primer/src/Primer/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ import Primer.Eval (AvoidShadowing (AvoidShadowing))
import Primer.Eval qualified as Eval
import Primer.Eval.Detail (EvalDetail)
import Primer.Eval.Redex (EvalLog, RunRedexOptions (RunRedexOptions, pushAndElide), ViewRedexOptions (ViewRedexOptions, groupedLets))
import Primer.EvalFull (Dir (Syn), EvalFullError (TimedOut), TerminationBound, evalFull)
import Primer.EvalFullStep (Dir (Syn), EvalFullError (TimedOut), TerminationBound, evalFull)
import Primer.JSON
import Primer.Log (ConvertLogMessage)
import Primer.Module (
Expand Down
4 changes: 4 additions & 0 deletions primer/src/Primer/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Primer.Core (
CaseFallback' (..),
caseBranchName,
traverseFallback,
mapFallback,
module Primer.Core.Meta,
module Primer.Core.Type,
TypeCache (..),
Expand Down Expand Up @@ -348,6 +349,9 @@ traverseFallback f = \case
CaseExhaustive -> pure CaseExhaustive
CaseFallback e -> CaseFallback <$> f e

mapFallback :: (Expr' a b c -> Expr' a' b' c') -> CaseFallback' a b c -> CaseFallback' a' b' c'
mapFallback f = runIdentity . traverseFallback (Identity . f)

-- | Variable bindings
-- These are used in case branches to represent the binding of a variable.
-- They aren't currently used in lambdas or lets, but in the future that may change.
Expand Down
8 changes: 7 additions & 1 deletion primer/src/Primer/Core/Type/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module Primer.Core.Type.Utils (
freeVarsTy,
boundVarsTy,
alphaEqTy,
alphaEqTy',
concreteTy,
) where

Expand Down Expand Up @@ -130,7 +131,12 @@ boundVarsTy = foldMap' getBoundHereDnTy . universe
-- Note that we do not expand TLets, they must be structurally
-- the same (perhaps with a different named binding)
alphaEqTy :: Type' () () -> Type' () () -> Bool
alphaEqTy = go (0, mempty, mempty)
alphaEqTy = alphaEqTy' (0, mempty, mempty)

-- Check two types for alpha equality where each may be from a
-- different alpha-related context
alphaEqTy' :: (Int, Map TyVarName Int, Map TyVarName Int) -> Type' () () -> Type' () () -> Bool
alphaEqTy' = go
where
go _ (TEmptyHole _) (TEmptyHole _) = True
go bs (THole _ s) (THole _ t) = go bs s t
Expand Down
82 changes: 80 additions & 2 deletions primer/src/Primer/Core/Utils.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{-# LANGUAGE ViewPatterns #-}

module Primer.Core.Utils (
freshLocalName,
freshLocalName',
Expand All @@ -23,15 +25,19 @@ module Primer.Core.Utils (
freeGlobalVars,
alphaEqTy,
concreteTy,
alphaEq,
freshen,
) where

import Foreword

import Control.Monad.Fresh (MonadFresh, fresh)
import Data.Data (Data)
import Data.Generics.Uniplate.Data (universe)
import Data.Map.Strict qualified as M
import Data.Set qualified as S
import Data.Set.Optics (setOf)
import Data.Tuple.Extra (firstM)
import Optics (
Fold,
Traversal,
Expand All @@ -51,13 +57,14 @@ import Optics (

import Primer.Core (
CaseBranch' (..),
CaseFallback' (CaseExhaustive, CaseFallback),
Expr,
Expr' (..),
GVarName,
HasID (_id),
ID,
LVarName,
LocalName (unLocalName),
LocalName (LocalName, unLocalName),
TmVarRef (GlobalVarRef, LocalVarRef),
TyVarName,
Type' (..),
Expand All @@ -72,6 +79,7 @@ import Primer.Core (
import Primer.Core.Fresh (freshLocalName, freshLocalName')
import Primer.Core.Type.Utils (
alphaEqTy,
alphaEqTy',
boundVarsTy,
concreteTy,
forgetKindMetadata,
Expand All @@ -86,7 +94,7 @@ import Primer.Core.Type.Utils (
typeIDs,
_freeVarsTy,
)
import Primer.Name (Name)
import Primer.Name (Name (unName), unsafeMkName)

-- | Regenerate all IDs (including in types and kinds), not changing any other metadata
regenerateExprIDs :: (HasID a, HasID b, HasID c, MonadFresh ID m) => Expr' a b c -> m (Expr' a b c)
Expand Down Expand Up @@ -194,3 +202,73 @@ freeGlobalVars e = S.fromList [v | Var _ (GlobalVarRef v) <- universe e]
-- | Traverse the 'ID's in an 'Expr''.
exprIDs :: (HasID a, HasID b, HasID c) => Traversal' (Expr' a b c) ID
exprIDs = (_exprMeta % _id) `adjoin` (_exprTypeMeta % _id) `adjoin` (_exprKindMeta % _id)

-- Check two terms for alpha equality
--
-- it makes usage easier if this is pure
-- i.e. we don't want to need a fresh name supply
-- We assume both inputs are both from the same context
--
-- Note that we do not expand let bindings, they must be structurally
-- the same (perhaps with a different named binding)
alphaEq :: Expr' () () () -> Expr' () () () -> Bool
alphaEq = go (0, mempty, mempty)
where
go bs (Hole _ t1) (Hole _ t2) = go bs t1 t2
go _ (EmptyHole _) (EmptyHole _) = True
go bs (Ann _ t1 ty1) (Ann _ t2 ty2) = go bs t1 t2 && alphaEqTy' (extractTypeEnv bs) ty1 ty2
go bs (App _ f1 t1) (App _ f2 t2) = go bs f1 f2 && go bs t1 t2
go bs (APP _ e1 ty1) (APP _ e2 ty2) = go bs e1 e2 && alphaEqTy' (extractTypeEnv bs) ty1 ty2
go bs (Con _ c1 as1) (Con _ c2 as2) = c1 == c2 && length as1 == length as2 && and (zipWith (go bs) as1 as2)
go bs (Lam _ v1 t1) (Lam _ v2 t2) = go (newTm bs v1 v2) t1 t2
go bs (LAM _ v1 t1) (LAM _ v2 t2) = go (newTy bs v1 v2) t1 t2
go (_, bs1, bs2) (Var _ (LocalVarRef v1)) (Var _ (LocalVarRef v2)) = bs1 ! Left v1 == bs2 ! Left v2
go _ (Var _ (GlobalVarRef v1)) (Var _ (GlobalVarRef v2)) = v1 == v2
go bs (Let _ v1 s1 t1) (Let _ v2 s2 t2) = go bs s1 s2 && go (newTm bs v1 v2) t1 t2
go bs (LetType _ v1 ty1 t1) (LetType _ v2 ty2 t2) = alphaEqTy' (extractTypeEnv bs) ty1 ty2 && go (newTy bs v1 v2) t1 t2
go bs (Letrec _ v1 t1 ty1 e1) (Letrec _ v2 t2 ty2 e2) =
go (newTm bs v1 v2) t1 t2
&& alphaEqTy' (extractTypeEnv bs) ty1 ty2
&& go (newTm bs v1 v2) e1 e2
go bs (Case _ e1 brs1 fb1) (Case _ e2 brs2 fb2) =
go bs e1 e2
&& and
( zipWith
( \(CaseBranch c1 (fmap bindName -> vs1) t1)
(CaseBranch c2 (fmap bindName -> vs2) t2) ->
c1
== c2
&& length vs1
== length vs2
&& go (foldl' (uncurry . newTm) bs $ zip vs1 vs2) t1 t2
)
brs1
brs2
)
&& case (fb1, fb2) of
(CaseExhaustive, CaseExhaustive) -> True
(CaseFallback f1, CaseFallback f2) -> go bs f1 f2
_ -> False
go _ (PrimCon _ c1) (PrimCon _ c2) = c1 == c2
go _ _ _ = False
p ! n = case p M.!? n of
Nothing -> Left n -- free vars: compare by name
Just i -> Right i -- bound vars: up to alpha
-- Note that the maps 'p' and 'q' map names to "which forall
-- they came from", in some sense. The @c@ value is how many
-- binders we have gone under, and is thus the next value free
-- in the map.
new (c, bs1, bs2) n m = (c + 1 :: Int, M.insert n c bs1, M.insert m c bs2)
newTm bs v1 v2 = new bs (Left v1) (Left v2)
newTy bs v1 v2 = new bs (Right v1) (Right v2)
extractTypeEnv (c, bs1, bs2) = let f = M.fromList . mapMaybe (firstM rightToMaybe) . M.assocs in (c, f bs1, f bs2)

freshen :: Set Name -> LocalName k -> LocalName k
freshen fvs n = go (0 :: Int)
where
go i =
let suffix = if i > 0 then "_" <> show i else ""
m = LocalName $ unsafeMkName $ unName (unLocalName n) <> suffix
in if unLocalName m `elem` fvs
then go (i + 1)
else m
Loading

1 comment on commit 6b8368f

@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: 6b8368f Previous: 9865b85 Ratio
evalTestM/discard logs/mapEven 1: outlier variance 0.07136874927824288 outlier variance 0.024375 outlier variance 2.93

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

CC: @dhess

Please sign in to comment.