Improve encoding/decoding speed (#1500)

... by not going through a `Term` intermediate

This gives a ~28% performance in decoding improvement, which means that
cache looks are not faster.

Here are the new decoding benchmarks before and after this change:

Before:

```
benchmarked Issue #108/Binary
time                 266.5 μs   (265.7 μs .. 267.4 μs)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 266.3 μs   (265.6 μs .. 267.1 μs)
std dev              2.418 μs   (1.891 μs .. 3.436 μs)

benchmarking Kubernetes/Binary ... took 36.94 s, total 56 iterations
benchmarked Kubernetes/Binary
time                 641.3 ms   (623.0 ms .. 655.4 ms)
                     0.999 R²   (0.997 R² .. 1.000 R²)
mean                 679.7 ms   (665.5 ms .. 702.6 ms)
std dev              29.48 ms   (14.15 ms .. 39.05 ms)
```

After:

```
benchmarked Issue #108/Binary
time                 282.2 μs   (279.6 μs .. 284.7 μs)
                     1.000 R²   (0.999 R² .. 1.000 R²)
mean                 281.9 μs   (280.7 μs .. 287.7 μs)
std dev              7.089 μs   (2.550 μs .. 15.44 μs)
variance introduced by outliers: 11% (moderately inflated)

benchmarking Kubernetes/Binary ... took 27.57 s, total 56 iterations
benchmarked Kubernetes/Binary
time                 499.1 ms   (488.1 ms .. 506.6 ms)
                     0.999 R²   (0.998 R² .. 1.000 R²)
mean                 498.9 ms   (494.4 ms .. 503.9 ms)
std dev              8.539 ms   (6.236 ms .. 12.56 ms)
```

There's a slight performance regression for the decoding microbenchmark, but
in practice my testing on real examples matches performance improvements seen
in the larger benchmark based on an example cache product from
`dhall-kubernetes`.

Note that is a breaking change because:

* There is no longer a `FromTerm` nor `ToTerm` class.  Now we use the
  `Serialise` class and `{encode,decode}Expression` now work on `ByteString`s
  instead of `Term`s

* I further narrowed the types of several encoding/decoding utilites to expect a
  `Void` for the first type parameter of `Expr`

* This is a regression with respect to stripping 55799 CBOR tags, mainly
  because properly handling the tags at every possible point in the syntax tree
  would considerably complicate the code
This commit is contained in:
Gabriel Gonzalez 2019-10-31 20:05:22 -07:00 committed by GitHub
parent 5ceb8d9d60
commit b843cae5d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1136 additions and 896 deletions

View File

@ -165,5 +165,5 @@ normalize (WellTyped expr) = Normal $ Dhall.normalize expr
-- Dhall's hash annotations (prefixed by "sha256:" and base-64 encoded).
hashNormalToCode :: Normal -> Text
hashNormalToCode (Normal expr) =
Dhall.hashExpressionToCode alphaNormal
Dhall.hashExpressionToCode (Dhall.denote alphaNormal)
where alphaNormal = Dhall.alphaNormalize expr

Binary file not shown.

View File

@ -6,7 +6,8 @@ module Main where
import Control.Monad (forM)
import Data.Map (Map, foldrWithKey, singleton, unions)
import Data.Monoid ((<>))
import Gauge (defaultMain, bgroup, bench, whnf, nfIO)
import Data.Void (Void)
import Gauge (defaultMain, bgroup, bench, nf, whnf, nfIO)
import System.Directory
@ -67,27 +68,25 @@ benchExprFromText name expr =
benchExprFromBytes
:: String -> Data.ByteString.Lazy.ByteString -> Gauge.Benchmark
benchExprFromBytes name bytes = bench name (whnf f bytes)
benchExprFromBytes name bytes = bench name (nf f bytes)
where
f bytes = do
term <- case Codec.Serialise.deserialiseOrFail bytes of
Left _ -> Nothing
Right term -> return term
case Dhall.Binary.decodeExpression term
:: Either Dhall.Binary.DecodingFailure (Dhall.Expr () Dhall.Import) of
Left _ -> Nothing
Right expression -> return expression
case Dhall.Binary.decodeExpression bytes of
Left exception -> error (show exception)
Right expression -> expression :: Dhall.Expr Void Dhall.Import
main :: IO ()
main = do
prelude <- loadPreludeFiles
issue108Text <- TIO.readFile "benchmark/examples/issue108.dhall"
issue108Bytes <- Data.ByteString.Lazy.readFile "benchmark/examples/issue108.dhall.bin"
kubernetesExample <- Data.ByteString.Lazy.readFile "benchmark/examples/kubernetes.dhall.bin"
defaultMain
[ bgroup "Issue #108"
[ benchExprFromText "Text" issue108Text
, benchExprFromBytes "Binary" issue108Bytes
]
, benchExprFromBytes "Kubernetes/Binary" kubernetesExample
, benchExprFromText "Long variable names" (T.replicate 1000000 "x")
, benchExprFromText "Large number of function arguments" (T.replicate 10000 "x ")
, benchExprFromText "Long double-quoted strings" ("\"" <> T.replicate 1000000 "x" <> "\"")

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,6 @@ import Data.Text (Text)
import Data.Text.Prettyprint.Doc (Doc, Pretty)
import Data.Void (Void)
import Dhall.Syntax (Binding(..), Chunks (..), Const(..), DhallDouble(..), Expr(..), Var(..))
import Dhall.Binary (ToTerm)
import Dhall.Map (Map)
import Dhall.Set (Set)
import Dhall.Pretty.Internal (Ann)
@ -156,7 +155,7 @@ rparen :: Diff
rparen = token Internal.rparen
-- | Render the difference between the normal form of two expressions
diffNormalized :: (Eq a, Pretty a, ToTerm a) => Expr s a -> Expr s a -> Diff
diffNormalized :: (Eq a, Pretty a) => Expr s a -> Expr s a -> Diff
diffNormalized l0 r0 = Dhall.Diff.diff l1 r1
where
l1 = Dhall.Normalize.alphaNormalize (Dhall.Normalize.normalize l0)

View File

@ -62,7 +62,7 @@ freezeImport directory import_ = do
Dhall.Core.alphaNormalize (Dhall.Core.normalize expression)
-- make sure the frozen import is present in the semantic cache
Dhall.Import.writeExpressionToSemanticCache expression
Dhall.Import.writeExpressionToSemanticCache (Dhall.Core.denote expression)
let expressionHash = Dhall.Import.hashExpression normalizedExpression

View File

@ -138,7 +138,6 @@ module Dhall.Import (
) where
import Control.Applicative (Alternative(..))
import Codec.CBOR.Term (Term(..))
import Control.Exception (Exception, SomeException, IOException, toException)
import Control.Monad (when)
import Control.Monad.Catch (throwM, MonadCatch(catch), handle)
@ -180,6 +179,8 @@ import Dhall.Import.Types
import Dhall.Parser (Parser(..), ParseError(..), Src(..), SourcedException(..))
import Lens.Family.State.Strict (zoom)
import qualified Codec.CBOR.Encoding as Encoding
import qualified Codec.CBOR.Write as Write
import qualified Codec.Serialise
import qualified Control.Monad.Trans.Maybe as Maybe
import qualified Control.Monad.Trans.State.Strict as State
@ -504,6 +505,7 @@ loadImportWithSemanticCache
case mCached of
Just bytesStrict -> do
let actualHash = Dhall.Crypto.sha256Hash bytesStrict
if semanticHash == actualHash
then return ()
else do
@ -511,12 +513,10 @@ loadImportWithSemanticCache
throwMissingImport (Imported _stack (HashMismatch {expectedHash = semanticHash, ..}))
let bytesLazy = Data.ByteString.Lazy.fromStrict bytesStrict
term <- case Codec.Serialise.deserialiseOrFail bytesLazy of
Left err -> throwMissingImport (Imported _stack err)
Right t -> return t
importSemantics <- case Dhall.Binary.decodeExpression term of
Left err -> throwMissingImport (Imported _stack err)
Right sem -> return sem
importSemantics <- case Dhall.Binary.decodeExpression bytesLazy of
Left err -> throwMissingImport (Imported _stack err)
Right e -> return e
return (ImportSemantics {..})
@ -544,7 +544,7 @@ fetchFromSemanticCache expectedHash = Maybe.runMaybeT $ do
-- | Ensure that the given expression is present in the semantic cache. The
-- given expression should be alpha-beta-normal.
writeExpressionToSemanticCache :: Expr Src Void -> IO ()
writeExpressionToSemanticCache :: Expr Void Void -> IO ()
writeExpressionToSemanticCache expression = writeToSemanticCache hash bytes
where
bytes = encodeExpression NoVersion expression
@ -592,18 +592,14 @@ loadImportWithSemisemanticCache (Chained (Import (ImportHashed _ importType) Cod
-- Check the semi-semantic cache. See
-- https://github.com/dhall-lang/dhall-haskell/issues/1098 for the reasoning
-- behind semi-semantic caching.
let semisemanticHash = computeSemisemanticHash resolvedExpr
let semisemanticHash = computeSemisemanticHash (Dhall.Core.denote resolvedExpr)
mCached <- lift $ fetchFromSemisemanticCache semisemanticHash
importSemantics <- case mCached of
Just bytesStrict -> do
let bytesLazy = Data.ByteString.Lazy.fromStrict bytesStrict
term <- case Codec.Serialise.deserialiseOrFail bytesLazy of
Left err -> throwMissingImport (Imported _stack err)
Right t -> return t
importSemantics <- case Dhall.Binary.decodeExpression term of
importSemantics <- case Dhall.Binary.decodeExpression bytesLazy of
Left err -> throwMissingImport (Imported _stack err)
Right sem -> return sem
@ -659,7 +655,7 @@ loadImportWithSemisemanticCache (Chained (Import (ImportHashed _ importType) Loc
-- AST (without normalising or type-checking it first). See
-- https://github.com/dhall-lang/dhall-haskell/issues/1098 for further
-- discussion.
computeSemisemanticHash :: Expr Src Void -> Dhall.Crypto.SHA256Digest
computeSemisemanticHash :: Expr Void Void -> Dhall.Crypto.SHA256Digest
computeSemisemanticHash resolvedExpr = hashExpression resolvedExpr
-- Fetch encoded normal form from "semi-semantic cache"
@ -1012,7 +1008,7 @@ loadWith expr₀ = case expr₀ of
ImportSemantics {..} <- loadImport child
zoom stack (State.put _stack)
return importSemantics
return (Dhall.Core.renote importSemantics)
ImportAlt a b -> loadWith a `catch` handler
where
@ -1109,33 +1105,30 @@ loadRelativeTo rootDirectory semanticCacheMode expression =
(emptyStatus rootDirectory) { _semanticCacheMode = semanticCacheMode }
encodeExpression
:: forall s
. StandardVersion
:: StandardVersion
-- ^ `NoVersion` means to encode without the version tag
-> Expr s Void
-> Expr Void Void
-> Data.ByteString.ByteString
encodeExpression _standardVersion expression = bytesStrict
where
intermediateExpression :: Expr s Import
intermediateExpression :: Expr Void Import
intermediateExpression = fmap absurd expression
term :: Term
term = Dhall.Binary.encodeExpression intermediateExpression
taggedTerm :: Term
taggedTerm =
encoding =
case _standardVersion of
NoVersion -> term
s -> TList [ TString v, term ]
NoVersion ->
Codec.Serialise.encode intermediateExpression
s ->
Encoding.encodeListLen 2
<> Encoding.encodeString v
<> Codec.Serialise.encode intermediateExpression
where
v = Dhall.Binary.renderStandardVersion s
bytesLazy = Codec.Serialise.serialise taggedTerm
bytesStrict = Data.ByteString.Lazy.toStrict bytesLazy
bytesStrict = Write.toStrictByteString encoding
-- | Hash a fully resolved expression
hashExpression :: Expr s Void -> Dhall.Crypto.SHA256Digest
hashExpression :: Expr Void Void -> Dhall.Crypto.SHA256Digest
hashExpression expression =
Dhall.Crypto.sha256Hash (encodeExpression NoVersion expression)
@ -1145,7 +1138,7 @@ hashExpression expression =
In other words, the output of this function can be pasted into Dhall
source code to add an integrity check to an import
-}
hashExpressionToCode :: Expr s Void -> Text
hashExpressionToCode :: Expr Void Void -> Text
hashExpressionToCode expr =
"sha256:" <> Text.pack (show (hashExpression expr))

View File

@ -49,7 +49,7 @@ instance Pretty Chained where
-- | An import that has been fully interpeted
data ImportSemantics = ImportSemantics
{ importSemantics :: Expr Src Void
{ importSemantics :: Expr Void Void
-- ^ The fully resolved import, typechecked and beta-normal.
}

View File

@ -44,7 +44,6 @@ import Text.Dot ((.->.))
import qualified Codec.CBOR.JSON
import qualified Codec.CBOR.Read
import qualified Codec.CBOR.Write
import qualified Codec.Serialise
import qualified Control.Exception
import qualified Control.Monad.Trans.State.Strict as State
import qualified Data.Aeson
@ -673,9 +672,7 @@ command (Options {..}) = do
Encode {..} -> do
expression <- getExpression file
let term = Dhall.Binary.encodeExpression expression
let bytes = Codec.Serialise.serialise term
let bytes = Dhall.Binary.encodeExpression (Dhall.Core.denote expression)
if json
then do
@ -696,7 +693,7 @@ command (Options {..}) = do
InputFile f -> Data.ByteString.Lazy.readFile f
StandardInput -> Data.ByteString.Lazy.getContents
term <- do
expression <- do
if json
then do
value <- case Data.Aeson.eitherDecode' bytes of
@ -705,15 +702,14 @@ command (Options {..}) = do
let encoding = Codec.CBOR.JSON.encodeValue value
let cborBytes = Codec.CBOR.Write.toLazyByteString encoding
Dhall.Core.throws (Codec.Serialise.deserialiseOrFail cborBytes)
let cborgBytes = Codec.CBOR.Write.toLazyByteString encoding
Dhall.Core.throws (Dhall.Binary.decodeExpression cborgBytes)
else do
Dhall.Core.throws (Codec.Serialise.deserialiseOrFail bytes)
Dhall.Core.throws (Dhall.Binary.decodeExpression bytes)
expression <- Dhall.Core.throws (Dhall.Binary.decodeExpression term)
:: IO (Expr Src Import)
let doc = Dhall.Pretty.prettyCharacterSet characterSet expression
let doc = Dhall.Pretty.prettyCharacterSet characterSet (Dhall.Core.renote expression :: Expr Src Import)
renderDoc System.IO.stdout doc

View File

@ -211,7 +211,7 @@ newtype DhallDouble = DhallDouble { getDhallDouble :: Double }
--
-- This instance is also consistent with with the binary encoding of Dhall @Double@s:
--
-- >>> toBytes n = Codec.Serialise.serialise (Dhall.Binary.encode (n :: DhallDouble))
-- >>> toBytes n = Dhall.Binary.encodeExpression (DoubleLit n :: Expr Void Import)
--
-- prop> \a b -> (a == b) == (toBytes a == toBytes b)
instance Eq DhallDouble where

View File

@ -37,7 +37,6 @@ import Data.Text (Text)
import Data.Text.Prettyprint.Doc (Doc, Pretty(..))
import Data.Typeable (Typeable)
import Data.Void (Void, absurd)
import Dhall.Binary (ToTerm(..))
import Dhall.Context (Context)
import Dhall.Syntax (Binding(..), Const(..), Chunks(..), Expr(..), Var(..))
import Dhall.Eval
@ -1330,13 +1329,13 @@ data TypeMessage s a
| CantMultiply (Expr s a) (Expr s a)
deriving (Show)
shortTypeMessage :: (Eq a, Pretty a, ToTerm a) => TypeMessage s a -> Doc Ann
shortTypeMessage :: (Eq a, Pretty a) => TypeMessage s a -> Doc Ann
shortTypeMessage msg =
"\ESC[1;31mError\ESC[0m: " <> short <> "\n"
where
ErrorMessages {..} = prettyTypeMessage msg
longTypeMessage :: (Eq a, Pretty a, ToTerm a) => TypeMessage s a -> Doc Ann
longTypeMessage :: (Eq a, Pretty a) => TypeMessage s a -> Doc Ann
longTypeMessage msg =
"\ESC[1;31mError\ESC[0m: " <> short <> "\n"
<> "\n"
@ -1357,8 +1356,7 @@ _NOT = "\ESC[1mnot\ESC[0m"
insert :: Pretty a => a -> Doc Ann
insert = Dhall.Util.insert
prettyTypeMessage
:: (Eq a, Pretty a, ToTerm a) => TypeMessage s a -> ErrorMessages
prettyTypeMessage :: (Eq a, Pretty a) => TypeMessage s a -> ErrorMessages
prettyTypeMessage (UnboundVariable x) = ErrorMessages {..}
-- We do not need to print variable name here. For the discussion see:
-- https://github.com/dhall-lang/dhall-haskell/pull/116
@ -4252,12 +4250,12 @@ data TypeError s a = TypeError
, typeMessage :: TypeMessage s a
}
instance (Eq a, Pretty s, Pretty a, ToTerm a) => Show (TypeError s a) where
instance (Eq a, Pretty s, Pretty a) => Show (TypeError s a) where
show = Pretty.renderString . Pretty.layoutPretty layoutOpts . Pretty.pretty
instance (Eq a, Pretty s, Pretty a, ToTerm a, Typeable s, Typeable a) => Exception (TypeError s a)
instance (Eq a, Pretty s, Pretty a, Typeable s, Typeable a) => Exception (TypeError s a)
instance (Eq a, Pretty s, Pretty a, ToTerm a) => Pretty (TypeError s a) where
instance (Eq a, Pretty s, Pretty a) => Pretty (TypeError s a) where
pretty (TypeError _ expr msg)
= Pretty.unAnnotate
( "\n"
@ -4425,12 +4423,12 @@ messageExpressions f m = case m of
newtype DetailedTypeError s a = DetailedTypeError (TypeError s a)
deriving (Typeable)
instance (Eq a, Pretty s, Pretty a, ToTerm a) => Show (DetailedTypeError s a) where
instance (Eq a, Pretty s, Pretty a) => Show (DetailedTypeError s a) where
show = Pretty.renderString . Pretty.layoutPretty layoutOpts . Pretty.pretty
instance (Eq a, Pretty s, Pretty a, ToTerm a, Typeable s, Typeable a) => Exception (DetailedTypeError s a)
instance (Eq a, Pretty s, Pretty a, Typeable s, Typeable a) => Exception (DetailedTypeError s a)
instance (Eq a, Pretty s, Pretty a, ToTerm a) => Pretty (DetailedTypeError s a) where
instance (Eq a, Pretty s, Pretty a) => Pretty (DetailedTypeError s a) where
pretty (DetailedTypeError (TypeError ctx expr msg))
= Pretty.unAnnotate
( "\n"

View File

@ -10,9 +10,6 @@ import Prelude hiding (FilePath)
import Test.Tasty (TestTree)
import Turtle (FilePath, (</>))
import qualified Codec.CBOR.Read as CBOR
import qualified Codec.CBOR.Term as CBOR
import qualified Codec.Serialise as Serialise
import qualified Control.Monad as Monad
import qualified Data.Bifunctor as Bifunctor
import qualified Data.ByteString as ByteString
@ -26,6 +23,7 @@ import qualified Dhall.Parser as Parser
import qualified Dhall.Test.Util as Test.Util
import qualified Test.Tasty as Tasty
import qualified Test.Tasty.HUnit as Tasty.HUnit
import qualified Text.Printf as Printf
import qualified Turtle
parseDirectory :: FilePath
@ -135,16 +133,16 @@ shouldParse path = do
expression <- Core.throws (Parser.exprFromText mempty text)
let term = Binary.encodeExpression expression
let bytes = Binary.encodeExpression (Core.denote expression)
let bytes = Serialise.serialise term
let render =
concatMap (Printf.printf "%02x ")
. ByteString.Lazy.unpack
Monad.unless (encoded == bytes) $ do
("", expected) <- Core.throws (CBOR.deserialiseFromBytes CBOR.decodeTerm encoded)
let message = "The expected CBOR representation doesn't match the actual one\n"
++ "expected: " ++ show expected ++ "\n but got: " ++ show term
++ "\n expr: " ++ show expression
++ "expected: " ++ render encoded ++ "\n but got: " ++ render bytes
++ "\n expr: " ++ show (Core.denote expression :: Expr Void Import)
Tasty.HUnit.assertFailure message
@ -202,16 +200,29 @@ shouldNotParse path = do
shouldDecode :: Text -> TestTree
shouldDecode pathText = do
let expectedFailures = []
let expectedFailures =
[ {- Note that this test actually successfully decodes the value, but
mistakenly decodes the value to `_` instead of `x`. This is
because the 55799 tag causes normal decoding to fail, so it falls
back to treating the `"x"` as a version tag instead of a label.
Either way, fixing 55799 decoding would cause this test to pass
again.
-}
binaryDecodeDirectory </> "success/unit/SelfDescribeCBORX2"
, binaryDecodeDirectory </> "success/unit/SelfDescribeCBORX3"
]
let pathString = Text.unpack pathText
Test.Util.testCase pathText expectedFailures (do
bytes <- ByteString.Lazy.readFile (pathString <> "A.dhallb")
term <- Core.throws (Serialise.deserialiseOrFail bytes)
decodedExpression <- Core.throws (Binary.decodeExpression term)
decodedExpression <- case Binary.decodeExpression bytes of
Left exception ->
Tasty.HUnit.assertFailure (show exception)
Right decodedExpression ->
return decodedExpression
text <- Text.IO.readFile (pathString <> "B.dhall")
@ -223,7 +234,7 @@ shouldDecode pathText = do
let message =
"The decoded expression didn't match the parsed expression"
Tasty.HUnit.assertEqual message decodedExpression strippedExpression )
Tasty.HUnit.assertEqual message strippedExpression decodedExpression )
shouldNotDecode :: Text -> TestTree
shouldNotDecode pathText = do
@ -234,8 +245,6 @@ shouldNotDecode pathText = do
Test.Util.testCase pathText expectedFailures (do
bytes <- ByteString.Lazy.readFile (pathString <> ".dhallb")
term <- Core.throws (Serialise.deserialiseOrFail bytes)
case Binary.decodeExpression term :: Either Binary.DecodingFailure (Expr Void Import) of
case Binary.decodeExpression bytes :: Either Binary.DecodingFailure (Expr Void Import) of
Left _ -> return ()
Right _ -> Tasty.HUnit.assertFailure "Unexpected successful decode" )

View File

@ -10,10 +10,10 @@
module Dhall.Test.QuickCheck where
import Codec.Serialise (DeserialiseFailure(..))
import Data.Either (isRight)
import Data.Either.Validation (Validation(..))
import Data.Monoid ((<>))
import Data.Void (Void)
import Dhall (ToDhall(..), FromDhall(..), auto, extract, inject, embed, Vector)
import Dhall.Map (Map)
import Dhall.Core
@ -53,8 +53,6 @@ import Test.Tasty.QuickCheck (QuickCheckTests(..))
import Text.Megaparsec (SourcePos(..), Pos)
import qualified Control.Spoon
import qualified Codec.Serialise
import qualified Data.Coerce
import qualified Data.List
import qualified Data.Sequence
import qualified Data.SpecialValues
@ -78,13 +76,6 @@ import qualified Test.Tasty
import qualified Test.Tasty.QuickCheck
import qualified Text.Megaparsec as Megaparsec
newtype DeserialiseFailureWithEq = D DeserialiseFailure
deriving (Show)
instance Eq DeserialiseFailureWithEq where
D (DeserialiseFailure aL bL) == D (DeserialiseFailure aR bR) =
aL == aR && bL == bR
instance (Arbitrary a, Ord a) => Arbitrary (Set a) where
arbitrary = Dhall.Set.fromList <$> arbitrary
shrink = map Dhall.Set.fromList . shrink . Dhall.Set.toList
@ -412,21 +403,11 @@ instance Arbitrary Var where
binaryRoundtrip :: Expr () Import -> Property
binaryRoundtrip expression =
wrap
(fmap
Dhall.Binary.decodeExpression
(Codec.Serialise.deserialiseOrFail
(Codec.Serialise.serialise
(Dhall.Binary.encodeExpression expression)
)
)
)
=== wrap (Right (Right (Dhall.Core.denote expression :: Expr () Import)))
Dhall.Binary.decodeExpression (Dhall.Binary.encodeExpression denotedExpression)
=== Right denotedExpression
where
wrap
:: Either DeserialiseFailure a
-> Either DeserialiseFailureWithEq a
wrap = Data.Coerce.coerce
denotedExpression :: Expr Void Import
denotedExpression = Dhall.Core.denote expression
everythingWellTypedNormalizes :: Expr () () -> Property
everythingWellTypedNormalizes expression =