Added support for custom evaluation to Analyzer. (#375)

This commit is contained in:
Martin Šošić 2021-12-07 14:44:01 +01:00 committed by GitHub
parent bb3964ae6f
commit 880cd39226
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 330 additions and 161 deletions

View File

@ -141,3 +141,4 @@ tests:
- path - path
- unordered-containers - unordered-containers
- strong-path - strong-path
- split

View File

@ -30,9 +30,11 @@
-- @Page { title = "Home", author = Nothing, content = "Hello world" }@ -- @Page { title = "Home", author = Nothing, content = "Hello world" }@
module Wasp.Analyzer.Evaluator.Evaluation module Wasp.Analyzer.Evaluator.Evaluation
( runEvaluation, ( runEvaluation,
evaluation,
evaluation',
module Wasp.Analyzer.Evaluator.Evaluation.Combinators, module Wasp.Analyzer.Evaluator.Evaluation.Combinators,
) )
where where
import Wasp.Analyzer.Evaluator.Evaluation.Combinators import Wasp.Analyzer.Evaluator.Evaluation.Combinators
import Wasp.Analyzer.Evaluator.Evaluation.Internal (runEvaluation) import Wasp.Analyzer.Evaluator.Evaluation.Internal (evaluation, evaluation', runEvaluation)

View File

@ -1,10 +1,11 @@
module Wasp.Analyzer.Evaluator.EvaluationError module Wasp.Analyzer.Evaluator.EvaluationError
( EvaluationError (..), ( EvaluationError (..),
EvaluationErrorContext (..), EvaluationErrorContext (..),
EvaluationParseError (..),
) )
where where
import Text.Parsec (ParseError) import qualified Text.Parsec
import Wasp.Analyzer.Type (Type) import Wasp.Analyzer.Type (Type)
data EvaluationError data EvaluationError
@ -20,9 +21,10 @@ data EvaluationError
InvalidEnumVariant String String InvalidEnumVariant String String
| -- | "MissingField fieldName" | -- | "MissingField fieldName"
MissingField String MissingField String
| -- | In case when evaluation includes parsing with Parsec and it fails. | -- | In case when evaluation includes parsing and it fails.
ParseError ParseError ParseError EvaluationParseError
| WithContext EvaluationErrorContext EvaluationError | -- | Not an actual error, but a wrapper that provides additional context.
WithContext EvaluationErrorContext EvaluationError
deriving (Show, Eq) deriving (Show, Eq)
data EvaluationErrorContext data EvaluationErrorContext
@ -32,3 +34,10 @@ data EvaluationErrorContext
| -- | ForVariable varName | -- | ForVariable varName
ForVariable String ForVariable String
deriving (Show, Eq) deriving (Show, Eq)
data EvaluationParseError
= -- | In case when evaluation includes parsing with Parsec and it fails.
EvaluationParseErrorParsec Text.Parsec.ParseError
| -- | In case when evaluation does some general parsing and it fails.
EvaluationParseError String
deriving (Show, Eq)

View File

@ -5,7 +5,7 @@ module Wasp.Analyzer.StdTypeDefinitions.Entity () where
import Control.Arrow (left) import Control.Arrow (left)
import qualified Text.Parsec as Parsec import qualified Text.Parsec as Parsec
import qualified Wasp.Analyzer.Evaluator.EvaluationError as EvaluationError import qualified Wasp.Analyzer.Evaluator.EvaluationError as ER
import qualified Wasp.Analyzer.Type as Type import qualified Wasp.Analyzer.Type as Type
import qualified Wasp.Analyzer.TypeChecker.AST as TC.AST import qualified Wasp.Analyzer.TypeChecker.AST as TC.AST
import Wasp.Analyzer.TypeDefinitions (DeclType (..), IsDeclType (..)) import Wasp.Analyzer.TypeDefinitions (DeclType (..), IsDeclType (..))
@ -24,6 +24,6 @@ instance IsDeclType Entity where
declEvaluate _ _ expr = case expr of declEvaluate _ _ expr = case expr of
TC.AST.PSL pslString -> TC.AST.PSL pslString ->
left EvaluationError.ParseError $ left (ER.ParseError . ER.EvaluationParseErrorParsec) $
makeEntity <$> Parsec.parse Wasp.Psl.Parser.Model.body "" pslString makeEntity <$> Parsec.parse Wasp.Psl.Parser.Model.body "" pslString
_ -> Left $ EvaluationError.ExpectedType (Type.QuoterType "psl") (TC.AST.exprType expr) _ -> Left $ ER.ExpectedType (Type.QuoterType "psl") (TC.AST.exprType expr)

View File

@ -19,7 +19,8 @@ module Wasp.Analyzer.TypeDefinitions
where where
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Wasp.Analyzer.TypeDefinitions.Class import Wasp.Analyzer.TypeDefinitions.Class.IsDeclType
import Wasp.Analyzer.TypeDefinitions.Class.IsEnumType
import Wasp.Analyzer.TypeDefinitions.Internal import Wasp.Analyzer.TypeDefinitions.Internal
empty :: TypeDefinitions empty :: TypeDefinitions

View File

@ -0,0 +1,20 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
module Wasp.Analyzer.TypeDefinitions.Class.HasCustomEvaluation
( HasCustomEvaluation (..),
)
where
import Data.Typeable (Typeable)
import Wasp.Analyzer.Evaluator.Evaluation.TypedExpr (TypedExprEvaluation)
import Wasp.Analyzer.Type (Type)
-- | For a Haskell type @a@, provides its Wasp @Type@ (via @waspType@ function)
-- | and its @Evaluation@ (via @evaluation@ function) that evaluates from
-- | @waspType@ into @a@.
-- | This class can be useful when using @makeDeclType@, since @makeDeclType@ checks
-- | for types with this class and if they have it, it uses their custom evaluation
-- | instead of trying to figure out their evaluation on its own.
class (Typeable a) => HasCustomEvaluation a where
waspType :: Type
evaluation :: TypedExprEvaluation a

View File

@ -1,8 +1,7 @@
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE AllowAmbiguousTypes #-}
module Wasp.Analyzer.TypeDefinitions.Class module Wasp.Analyzer.TypeDefinitions.Class.IsDeclType
( IsDeclType (..), ( IsDeclType (..),
IsEnumType (..),
) )
where where
@ -10,52 +9,42 @@ import Data.Typeable (Typeable)
import Wasp.Analyzer.Evaluator.Bindings (Bindings) import Wasp.Analyzer.Evaluator.Bindings (Bindings)
import Wasp.Analyzer.Evaluator.EvaluationError (EvaluationError) import Wasp.Analyzer.Evaluator.EvaluationError (EvaluationError)
import Wasp.Analyzer.TypeChecker.AST (TypedExpr) import Wasp.Analyzer.TypeChecker.AST (TypedExpr)
import Wasp.Analyzer.TypeDefinitions.Internal (DeclType, EnumType, TypeDefinitions) import Wasp.Analyzer.TypeDefinitions.Internal (DeclType, TypeDefinitions)
import qualified Wasp.AppSpec.Core.Decl as AppSpecDecl import qualified Wasp.AppSpec.Core.Decl as AppSpecDecl
-- | Marks Haskell type as a representation of a specific Wasp declaration type. -- | Marks Haskell type as a representation of a specific Wasp declaration type.
-- This is supposed to be used on types from Wasp AST (the IR between Analyzer and Generator) -- This is supposed to be used on types from @AppSpec@ (the main Wasp IR)
-- in order to enrich them with information on how are they to be analyzed -- in order to enrich them with information on how are they to be analyzed
-- (and therefore make them part of the Wasp language). -- (and therefore make them part of the Wasp language).
-- --
-- To put it very practically, if you make a Haskell type (usually from @AppSpec@)
-- an instance of this class, and if you add it to "Wasp.Analyzer.StdTypeDefinitions",
-- you are telling Analyzer that there is a new declaration type in Wasp language with
-- name, Wasp type and evaluation from that Wasp type into Haskell type as specified
-- by this instance, and therefore you just added a new declaration type to the
-- Wasp language and Analyzer will include it in the final result it produces.
--
-- NOTE: If this Haskell type satisfies certain requirements, the IsDeclType instance for it -- NOTE: If this Haskell type satisfies certain requirements, the IsDeclType instance for it
-- can be automatically derived from its shape by using 'Analyzer.Evaluator.TH.makeDeclType'. -- can be automatically derived from its shape by using
-- 'Wasp.Analyzer.TypeDefinitions.TH.makeDeclType'.
class (Typeable a, AppSpecDecl.IsDecl a) => IsDeclType a where class (Typeable a, AppSpecDecl.IsDecl a) => IsDeclType a where
declType :: DeclType declType :: DeclType
-- | Evaluates a given Wasp "TypedExpr" to a value of this type, assuming it is of -- | Evaluates a given Wasp "TypedExpr" to a value of type @a@, assuming given
-- declaration type described by (dtBodyType . declType) and (dtName . declType) (otherwise throws an error). -- typed expression is of declaration type described by @dtBodyType . declType@
-- and @dtName . declType@ (otherwise throws an error).
-- --
-- For @declEvaluate typeDefs bindings declBodyExpr@: -- For @declEvaluate typeDefs bindings declBodyExpr@:
-- - "typeDefs" is the type definitions used in the Analyzer -- - "typeDefs" is the type definitions used in the Analyzer
-- - "bindings" contains the values of all the declarations evaluated so far -- - "bindings" contains the values of all the declarations evaluated so far
-- - "declBodyExpr" is the expression describing declaration body, that should be evaluated by this function -- - "declBodyExpr" is the expression describing declaration body,
-- that should be evaluated by this function
-- --
-- __Examples__ -- __Examples__
-- --
-- Imagine that we have Wasp code @test Example 4@, and we have @instance IsDeclType Test@. -- Imagine that we have Wasp code @test Example 4@, and we have @instance IsDeclType Test@.
-- Here, @test@ is declaration type name, @Example@ is declaration name, and @4@ is declaration body. -- Here, @test@ is declaration type name, @Example@ is declaration name,
-- and @4@ is declaration body.
-- @declEvaluate@ function would then be called somewhat like: -- @declEvaluate@ function would then be called somewhat like:
-- @declEvaluate @Test typeDefs bindings (NumberLiteral 4)@ -- @declEvaluate @Test typeDefs bindings (NumberLiteral 4)@
declEvaluate :: TypeDefinitions -> Bindings -> TypedExpr -> Either EvaluationError a declEvaluate :: TypeDefinitions -> Bindings -> TypedExpr -> Either EvaluationError a
-- | Marks Haskell type as a representation of a specific Wasp enum type.
-- Analogous to IsDeclType, but for enums.
--
-- NOTE: If this Haskell type satisfies certain requirements, the IsEnumType instance for it
-- can be automatically derived from its shape by using 'Analyzer.Evaluator.TH.makeEnumType'.
class Typeable a => IsEnumType a where
enumType :: EnumType
-- | Converts a string to a Haskell value of this type.
--
-- @mapM_ enumEvaluate (etVariants enumType) == Right ()@ should be true
-- for all instances of "IsEnumType".
--
-- __Examples__
--
-- >>> data Example = Foo | Bar
-- >>> instance IsEnumType Example where {- omitted -}
-- >>> enumEvaluate "Foo"
-- Foo
enumEvaluate :: String -> Either EvaluationError a

View File

@ -0,0 +1,31 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
module Wasp.Analyzer.TypeDefinitions.Class.IsEnumType
( IsEnumType (..),
)
where
import Data.Typeable (Typeable)
import Wasp.Analyzer.Evaluator.EvaluationError (EvaluationError)
import Wasp.Analyzer.TypeDefinitions.Internal (EnumType)
-- | Marks Haskell type as a representation of a specific Wasp enum type.
-- Analogous to IsDeclType, but for enums.
--
-- NOTE: If this Haskell type satisfies certain requirements, the IsEnumType instance for it
-- can be automatically derived from its shape by using 'Analyzer.Evaluator.TH.makeEnumType'.
class Typeable a => IsEnumType a where
enumType :: EnumType
-- | Converts a string to a Haskell value of this type.
--
-- @mapM_ enumEvaluate (etVariants enumType) == Right ()@ should be true
-- for all instances of "IsEnumType".
--
-- __Examples__
--
-- >>> data Example = Foo | Bar
-- >>> instance IsEnumType Example where {- omitted -}
-- >>> enumEvaluate "Foo"
-- Foo
enumEvaluate :: String -> Either EvaluationError a

View File

@ -11,37 +11,89 @@ import Control.Applicative ((<|>))
import qualified Data.HashMap.Strict as H import qualified Data.HashMap.Strict as H
import Language.Haskell.TH import Language.Haskell.TH
import Language.Haskell.TH.Syntax (VarBangType) import Language.Haskell.TH.Syntax (VarBangType)
import Wasp.Analyzer.Evaluator.Evaluation import qualified Wasp.Analyzer.Evaluator.Evaluation as E
import qualified Wasp.Analyzer.Type as T import qualified Wasp.Analyzer.Type as T
import Wasp.Analyzer.TypeDefinitions (DeclType (..), EnumType (..), IsDeclType (..), IsEnumType (..)) import Wasp.Analyzer.TypeDefinitions (DeclType (..), EnumType (..), IsDeclType (..), IsEnumType (..))
import Wasp.Analyzer.TypeDefinitions.Class.HasCustomEvaluation (HasCustomEvaluation)
import qualified Wasp.Analyzer.TypeDefinitions.Class.HasCustomEvaluation as HasCustomEvaluation
import Wasp.Analyzer.TypeDefinitions.TH.Common import Wasp.Analyzer.TypeDefinitions.TH.Common
import Wasp.AppSpec.Core.Decl (makeDecl) import Wasp.AppSpec.Core.Decl (makeDecl)
import Wasp.AppSpec.Core.Ref (Ref) import Wasp.AppSpec.Core.Ref (Ref)
import qualified Wasp.AppSpec.ExtImport as AppSpec.ExtImport import qualified Wasp.AppSpec.ExtImport as AppSpec.ExtImport
import qualified Wasp.AppSpec.JSON as AppSpec.JSON import qualified Wasp.AppSpec.JSON as AppSpec.JSON
-- | @makeDeclType ''Type@ writes an @IsDeclType@ instance for @Type@. A type -- | @makeDeclType ''T@ generates an @IsDeclType@ instance for Haskell type @T@.
-- error is raised if @Type@ does not fit the criteria described below. -- A type error is raised if @T@ does not fit the criteria described below, which are
-- required for @makeDeclType@ to be able to automatically generate @IsDeclType@
-- instance for a type @T@.
-- --
-- Requirements on @Type@ for this to work: -- Requirements on the shape of type @T@ for this to work:
-- - The type must be an ADT with one constructor. -- - type @T@ must be an ADT with one constructor, which must have
-- - The type must have just one field OR use record syntax (in which case it can have multiple fields). -- just one field OR use record syntax (in which case it can have multiple record fields).
-- - type @T@ must have no type parameters.
-- Therefore, some valid types based on this would be:
-- - `data T = Foo String`
-- - `data T = Foo { a :: String, b :: Int }`
-- while some invalid types would be:
-- - `data T = Foo String | Bar Int`
-- - `data T = Foo String Int`
-- - `data T a = Foo a`
-- --
-- Properties that hold for @Type@ and its generated @IsDeclType@ instance: -- Further, there are requirements/limitations as to which types can @T@ contain in itself
-- - For the rest of the bullet points below, let's say that -- (be it as a type of the record field or as a type of the data constructor field).
-- @let bodyType = dtBodyType (declType \@Type)@. -- They can be:
-- - If @Type@ uses record syntax, then -- - Primitive: @String@, @Integer@, @Double@, @Bool@.
-- - @bodyType@ is a @Dict@ -- - @[a]@ where @a@ is an accepted type itself.
-- - If and only if there is a key @x@ in @bodyType@, -- - @Maybe a@ (but only as a type of a record field) where @a@ is an accepted type itself.
-- then @Type@ has a record @x@ with the same type. -- - A few special types from AppSpec:
-- - If a key @x@ is optional, then the record @x@ in @Type@ is a @Maybe@. -- - 'Wasp.AppSpec.ExtImport.ExtImport'
-- - If @Type@ is a simple ADT (not record) with one field, then @bodyType@ maps to the type of that field. -- - 'Wasp.AppSpec.JSON.JSON'
-- - @dtName (declType \@Type) == "type"@ -> the name of declaration type is the same as the name of @Type@ -- - @Wasp.AppSpec.Core.Ref a@ where @a@ is an instance of @IsDeclType@.
-- but with the first letter changed to lowercase. -- - A type that implements an @IsEnumType@ instance.
-- - A type with just one data constructor that is a record, and whose record field types
-- are all accepted types.
-- - A type that implements a @HasCustomEvaluation@ instance.
-- Place in implementation that is a source of truth for this is 'waspKindOfHaskellType'.
--
-- If all of the above requirements are met, @IsDeclType@ instance is generated.
-- Main parts of that instance are Wasp type ('Wasp.Analyzer.Type.Type')
-- of the declaration body (referred to as @WT@ from now on) and the evaluator
-- that evalutes from that Wasp type @WT@ into Haskell type @T@.
-- We can also describe @WT@ with @let WT = dtBodyType (declType \@T)@.
-- For the user of @makeDeclType@, it is important to understand how
-- @makeDeclType@ determines @WT@ based on the @T@, since @WT@ then determines
-- how will this new declaration type (that we described with @T@) manifest
-- in the Wasp language itself.
--
-- @WT@ is determined based on @T@ in the following manner:
-- - If @T@ is a record, then @WT@ is a @Dict@ with the same field keys as @T@ has,
-- and field types are determined as described below. recursively in this same manner.
-- Only exception is if type of field in record @T@ is @Maybe a@ -> in that case
-- type of corresponding field in @Dict@ @WT@ is determined from @a@ but field is
-- marked as optional (otherwise it is required).
-- - If @T@ is a simple ADT (not record) with just one field, then @WT@ is determined
-- directly from the type of that one field.
-- - When determining how types of fields (in record or in simple ADT) from @T@ map
-- to parts of @WT@, following approach is used:
-- - Primitives, list, ExtImport, JSON, enum, decl ref -> all those are pretty
-- straight-forwardly mapped onto corresponding types from `Wasp.Analyzer.Type`.
-- - Type of record field in @T@ maps to required field in @WT@ @Dict@ of
-- type that maps per rules mentioned above/below.
-- Only exception is if type of field in record @T@ is @Maybe a@ -> in that case
-- type of corresponding field in @Dict@ @WT@ is determined from @a@ but field is
-- marked as optional (otherwise it is required).
-- - If type of field is a record itself, then it is maped in a same way as
-- @T@ would be mapped -> as a @Dict@ with the same fields as the record.
-- Therefore this results in nested dictionaries.
-- - For types that have an instance of @HasCustomEvaluation@,
-- its @waspType@ method is directly used to obtain the Wasp type.
-- This allows us to take control over how Wasp type is determined for this specific
-- sub-part of @T@.
--
-- Finally, it is important to mention that name of the declaration type is the same
-- as the name of @T@ but with first letter changed to lowercase:
-- @dtName (declType \@MyType) == "myType"@
-- --
-- One way to summarize the crux of properties above is to say: If given @Type@ is a simple ADT with one field,
-- it is translated into a declaration which has body corresponding to that field. If instead @Type@
-- is a record, it is translated into a declaration whose body is a dictionary with same fields as the record.
-- --
-- __Examples__ -- __Examples__
-- --
@ -110,18 +162,19 @@ makeIsDeclTypeInstanceDefinition _ _ = fail "makeDeclType expects given type to
recordFieldsToNameTypePairs :: [VarBangType] -> [(Name, Type)] recordFieldsToNameTypePairs :: [VarBangType] -> [(Name, Type)]
recordFieldsToNameTypePairs = map $ \(fieldName, _, fieldType) -> (fieldName, fieldType) recordFieldsToNameTypePairs = map $ \(fieldName, _, fieldType) -> (fieldName, fieldType)
-- | Create an "IsDeclType" instance for types that have a single data constructor which has a single value, e.g. @data Type = Type x@. -- | Create an "IsDeclType" instance for types that have a single data constructor which has a single value,
-- e.g. @data Type = Type x@.
genIsDeclTypeInstanceDefinitionFromNormalDataConstructor :: Name -> Name -> Type -> Q [DecQ] genIsDeclTypeInstanceDefinitionFromNormalDataConstructor :: Name -> Name -> Type -> Q [DecQ]
genIsDeclTypeInstanceDefinitionFromNormalDataConstructor typeName dataConstructorName dataConstructorType = do genIsDeclTypeInstanceDefinitionFromNormalDataConstructor typeName dataConstructorName dataConstructorType = do
let evaluateE = [|runEvaluation $ $(conE dataConstructorName) <$> $(genEvaluationExprForHaskellType dataConstructorType)|] (bodyTypeE, evaluationE) <- genWaspTypeAndEvaluationForHaskellType dataConstructorType
let bodyTypeE = genWaspTypeFromHaskellType dataConstructorType let evaluateE = [|E.runEvaluation $ $(conE dataConstructorName) <$> $evaluationE|]
pure $ genIsDeclTypeInstanceDefinition typeName dataConstructorName bodyTypeE evaluateE pure $ genIsDeclTypeInstanceDefinition typeName dataConstructorName bodyTypeE evaluateE
-- | For decls with record constructors, i.e. @data Fields = Fields { a :: String, b :: String } -- | For decls with record constructors, i.e. @data Fields = Fields { a :: String, b :: String }
genIsDeclTypeInstanceDefinitionFromRecordDataConstructor :: Name -> Name -> [(Name, Type)] -> Q [DecQ] genIsDeclTypeInstanceDefinitionFromRecordDataConstructor :: Name -> Name -> [(Name, Type)] -> Q [DecQ]
genIsDeclTypeInstanceDefinitionFromRecordDataConstructor typeName dataConstructorName fields = do genIsDeclTypeInstanceDefinitionFromRecordDataConstructor typeName dataConstructorName fields = do
(dictTypeE, dictEvaluationE) <- genDictTypeAndEvaluationForRecord dataConstructorName fields (dictTypeE, dictEvaluationE) <- genDictWaspTypeAndEvaluationForRecord dataConstructorName fields
let evaluateE = [|runEvaluation $dictEvaluationE|] let evaluateE = [|E.runEvaluation $dictEvaluationE|]
let bodyTypeE = dictTypeE let bodyTypeE = dictTypeE
pure $ genIsDeclTypeInstanceDefinition typeName dataConstructorName bodyTypeE evaluateE pure $ genIsDeclTypeInstanceDefinition typeName dataConstructorName bodyTypeE evaluateE
@ -142,96 +195,61 @@ genIsDeclTypeInstanceDefinition typeName dataConstructorName bodyTypeE evaluateE
genFunc 'declEvaluate evaluateE genFunc 'declEvaluate evaluateE
] ]
--------------- Dict ------------------ --------------- Kind, Wasp Type and Evaluation of a Haskell type ------------------
-- | Given a record data constructor name and fields, return the evaluation that evaluates type WaspTypeExpQ = ExpQ
-- Wasp dictionary into the given record, and also return the type of such Wasp dictionary.
-- First member of returned couple is type, second is evaluation.
genDictTypeAndEvaluationForRecord :: Name -> [(Name, Type)] -> Q (ExpQ, ExpQ)
genDictTypeAndEvaluationForRecord dataConName fields = do
(dictEntryTypesE, dictEvaluationE) <- genDictEntriesTypeAndEvaluationForRecord dataConName fields
return
( [|T.DictType $ H.fromList $dictEntryTypesE|],
[|dict $dictEvaluationE|]
)
genDictEntriesTypeAndEvaluationForRecord :: Name -> [(Name, Type)] -> Q (ExpQ, ExpQ) type EvaluationExpQ = ExpQ
genDictEntriesTypeAndEvaluationForRecord dataConstructorName fields =
go $ reverse fields -- Reversing enables us to apply evaluations in right order.
where
go [] = pure (listE [], [|pure|] `appE` conE dataConstructorName)
go ((fieldName, fieldType) : restOfFields) = do
(restDictType, restEvaluation) <- go restOfFields
let thisDictTypeE =
[|
($(nameToStringLiteralExpr fieldName), $(genDictEntryTypeFromHaskellType fieldType)) :
$restDictType
|]
let thisEvaluationE = [|$restEvaluation <*> $(genDictEntryEvaluationForRecordField fieldName fieldType)|]
pure (thisDictTypeE, thisEvaluationE)
-- | Write a @DictEntryType@ that corresponds to a given a Haskell type. -- | For a given Haskell type @typ@, generates two TH expressions:
genDictEntryTypeFromHaskellType :: Type -> ExpQ -- one that is a Wasp @Type@, and another that is @Evaluation@ that evaluates from that Wasp @Type@
genDictEntryTypeFromHaskellType typ = -- into a given Haskell type @typ@.
waspKindOfType typ >>= \case genWaspTypeAndEvaluationForHaskellType :: Type -> Q (WaspTypeExpQ, EvaluationExpQ)
KOptional elemType -> [|T.DictOptional $(genWaspTypeFromHaskellType elemType)|] genWaspTypeAndEvaluationForHaskellType typ =
_ -> [|T.DictRequired $(genWaspTypeFromHaskellType typ)|] waspKindOfHaskellType typ >>= \case
KString -> return ([|T.StringType|], [|E.string|])
-- | "genDictEvaluationE fieldName typ" writes a "DictEvaluation" for a haskell record field KInteger -> return ([|T.NumberType|], [|E.integer|])
-- named "fieldName" with a value "typ". KDouble -> return ([|T.NumberType|], [|E.double|])
genDictEntryEvaluationForRecordField :: Name -> Type -> ExpQ KBool -> return ([|T.BoolType|], [|E.bool|])
genDictEntryEvaluationForRecordField fieldName fieldType = KList elemHaskellType -> do
waspKindOfType fieldType >>= \case (elemWaspType, elemEvaluation) <- genWaspTypeAndEvaluationForHaskellType elemHaskellType
KOptional elemType -> [|maybeField $(nameToStringLiteralExpr fieldName) $(genEvaluationExprForHaskellType elemType)|] return ([|T.ListType $(elemWaspType)|], [|E.list $(elemEvaluation)|])
_ -> [|field $(nameToStringLiteralExpr fieldName) $(genEvaluationExprForHaskellType fieldType)|] KImport -> return ([|T.ExtImportType|], [|E.extImport|])
KJSON -> return ([|T.QuoterType "json"|], [|E.json|])
---------------------------------------- KDeclRef t ->
return
--------------- Types ------------------ ( [|T.DeclType $ dtName $ declType @ $(pure t)|],
[|E.declRef @ $(pure t)|]
-- | Haskell type -> Wasp type. )
genWaspTypeFromHaskellType :: Type -> ExpQ KEnum ->
genWaspTypeFromHaskellType typ = return
waspKindOfType typ >>= \case ( [|T.EnumType $ etName $ enumType @ $(pure typ)|],
KString -> [|T.StringType|] [|E.enum @ $(pure typ)|]
KInteger -> [|T.NumberType|] )
KDouble -> [|T.NumberType|] KRecord dataConName fields -> genDictWaspTypeAndEvaluationForRecord dataConName fields
KBool -> [|T.BoolType|] KCustomEvaluation ->
KList elemType -> [|T.ListType $(genWaspTypeFromHaskellType elemType)|] return
KImport -> [|T.ExtImportType|] ( [|HasCustomEvaluation.waspType @ $(pure typ)|],
KJSON -> [|T.QuoterType "json"|] [|HasCustomEvaluation.evaluation @ $(pure typ)|]
KDeclRef t -> [|T.DeclType $ dtName $ declType @ $(pure t)|] )
KEnum -> [|T.EnumType $ etName $ enumType @ $(pure typ)|]
KOptional _ -> fail "Maybe is only allowed in record fields" KOptional _ -> fail "Maybe is only allowed in record fields"
KRecord dataConName fields ->
fst =<< genDictTypeAndEvaluationForRecord dataConName fields
-- | Generates an expression that is @Evaluation@ that evaluates to a given Haskell type.
genEvaluationExprForHaskellType :: Type -> ExpQ
genEvaluationExprForHaskellType typ =
waspKindOfType typ >>= \case
KString -> [|string|]
KInteger -> [|integer|]
KDouble -> [|double|]
KBool -> [|bool|]
KList elemType -> [|list $(genEvaluationExprForHaskellType elemType)|]
KImport -> [|extImport|]
KJSON -> [|json|]
KDeclRef t -> [|declRef @ $(pure t)|]
KEnum -> [|enum @ $(pure typ)|]
KOptional _ -> fail "Maybe is only allowed in record fields"
KRecord dataConName fields ->
snd =<< genDictTypeAndEvaluationForRecord dataConName fields
-- | Find the "WaspKind" of a Haskell type. -- | Find the "WaspKind" of a Haskell type.
waspKindOfType :: Type -> Q WaspKind -- Wasp Kind is really just an intermediate representation that captures
waspKindOfType typ = do -- information we will need to later determine Wasp type and Evaluation for
-- this Haskell type, which will be used to evaluate type-checked AST.
waspKindOfHaskellType :: Type -> Q WaspKind
waspKindOfHaskellType typ = do
maybeDeclRefKind <- tryCastingToDeclRefKind typ maybeDeclRefKind <- tryCastingToDeclRefKind typ
maybeEnumKind <- tryCastingToEnumKind typ maybeEnumKind <- tryCastingToEnumKind typ
maybeCustomEvaluationKind <- tryCastingToCustomEvaluationKind typ
maybeRecordKind <- tryCastingToRecordKind typ maybeRecordKind <- tryCastingToRecordKind typ
maybe (fail $ "No translation to wasp type for type " ++ show typ) return $ maybe (fail $ "No translation to wasp type for type " ++ show typ) return $
maybeDeclRefKind maybeDeclRefKind
<|> maybeEnumKind <|> maybeEnumKind
-- NOTE: It is important that @maybeCustomEvaluationKind@ is before @maybeRecordKind@,
-- since having a custom evaluation should override typical record evalution, if type is a record.
<|> maybeCustomEvaluationKind
<|> maybeRecordKind <|> maybeRecordKind
<|> case typ of <|> case typ of
ConT name ConT name
@ -265,6 +283,11 @@ waspKindOfType typ = do
_ -> Nothing _ -> Nothing
tryCastingToRecordKind _ = return Nothing tryCastingToRecordKind _ = return Nothing
tryCastingToCustomEvaluationKind :: Type -> Q (Maybe WaspKind)
tryCastingToCustomEvaluationKind t = do
hasInstance <- isInstance ''HasCustomEvaluation [t]
return $ if hasInstance then Just KCustomEvaluation else Nothing
-- | An intermediate mapping between Haskell types and Wasp types, we use it internally -- | An intermediate mapping between Haskell types and Wasp types, we use it internally
-- in this module when generating @Types@, @Evaluation@, @DictEntryTypes@, and @DictEvaluation@ -- in this module when generating @Types@, @Evaluation@, @DictEntryTypes@, and @DictEvaluation@
-- so that we have easier time when figuring out what we are dealing with. -- so that we have easier time when figuring out what we are dealing with.
@ -284,5 +307,48 @@ data WaspKind
| -- | Type that has a single data constructor that is a record. | -- | Type that has a single data constructor that is a record.
-- KRecord <record constructor name> <fields:(identifier, type)> -- KRecord <record constructor name> <fields:(identifier, type)>
KRecord Name [(Name, Type)] KRecord Name [(Name, Type)]
| KCustomEvaluation
--------------------------------------- ---------- Kind, Wasp Type and Evaluation of a Haskell Record as a Wasp Dict -------------
-- | Given a record data constructor name and fields, return the evaluation that evaluates
-- Wasp dictionary into the given record, and also return the type of such Wasp dictionary.
-- First member of returned couple is type, second is evaluation.
genDictWaspTypeAndEvaluationForRecord :: Name -> [(Name, Type)] -> Q (WaspTypeExpQ, EvaluationExpQ)
genDictWaspTypeAndEvaluationForRecord dataConName fields = do
(dictEntryTypesE, dictEvaluationE) <- genDictEntriesWaspTypeAndEvaluationForRecord dataConName fields
return
( [|T.DictType $ H.fromList $dictEntryTypesE|],
[|E.dict $dictEvaluationE|]
)
genDictEntriesWaspTypeAndEvaluationForRecord :: Name -> [(Name, Type)] -> Q (WaspTypeExpQ, EvaluationExpQ)
genDictEntriesWaspTypeAndEvaluationForRecord dataConstructorName fields =
go $ reverse fields -- Reversing enables us to apply evaluations in right order.
where
go [] = pure (listE [], [|pure|] `appE` conE dataConstructorName)
go ((fieldName, fieldType) : restOfFields) = do
(fieldWaspType, fieldEvaluation) <- genDictEntryWaspTypeAndEvaluationForRecordField fieldName fieldType
(restWaspType, restEvaluation) <- go restOfFields
pure
( [|($(nameToStringLiteralExpr fieldName), $fieldWaspType) : $restWaspType|],
[|$restEvaluation <*> $fieldEvaluation|]
)
-- | For a given Haskell record field (name and type), generate Wasp Type and
-- Evaluation from that Wasp Type into given record field.
genDictEntryWaspTypeAndEvaluationForRecordField :: Name -> Type -> Q (WaspTypeExpQ, EvaluationExpQ)
genDictEntryWaspTypeAndEvaluationForRecordField fieldName fieldType =
waspKindOfHaskellType fieldType >>= \case
KOptional elemType -> do
(waspType, evaluation) <- genWaspTypeAndEvaluationForHaskellType elemType
return
( [|T.DictOptional $(waspType)|],
[|E.maybeField $(nameToStringLiteralExpr fieldName) $(evaluation)|]
)
_ -> do
(waspType, evaluation) <- genWaspTypeAndEvaluationForHaskellType fieldType
return
( [|T.DictRequired $(waspType)|],
[|E.field $(nameToStringLiteralExpr fieldName) $(evaluation)|]
)

View File

@ -18,18 +18,6 @@ data App = App
-- | NOTE: This is new. Before, `dependencies` was a standalone declaration and it was a {=json json=}, -- | NOTE: This is new. Before, `dependencies` was a standalone declaration and it was a {=json json=},
-- while now it is a [{ name :: String, version :: String }]. -- while now it is a [{ name :: String, version :: String }].
-- TODO: We might want to look into making this nicer in the future
-- -> maybe change how it is represented in the wasp lang, e.g. make it just a list of strings,
-- while here it would still be what it is or it would be parsed even more.
-- We could make this work by supporting manual definition of how Dependency evaluates / what is its type.
-- So, similar like we have IsDeclType instance for declarations, we could have similar abstraction for
-- sub-types like these, and we would inject it into Analyzer and it would be defined manually.
-- A way this might work: in Evaluator, we would check when the type matches the one we have this custom logic for,
-- probably in the function that figures out the Kind of the type, and then we would based on that use
-- evaluator provided for that type. This means we can provide this custom sort of logic per specific type from
-- AppSpec, but not for type + context in which it is. However, that should be enough, I don't think that is a problem.
-- If knowledge of context is important then it is best to just manually define the parent type's evaluation.
-- TODO: Make a github issue for this (TODO above)?
dependencies :: Maybe [Dependency] dependencies :: Maybe [Dependency]
} }
deriving (Show, Eq, Data) deriving (Show, Eq, Data)

View File

@ -1,16 +1,24 @@
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
module Analyzer.EvaluatorTest where module Analyzer.EvaluatorTest where
import Data.Data (Data) import Data.Data (Data)
import Data.List.Split (splitOn)
import Test.Tasty.Hspec import Test.Tasty.Hspec
import Text.Read (readMaybe)
import Wasp.Analyzer.Evaluator import Wasp.Analyzer.Evaluator
import qualified Wasp.Analyzer.Evaluator.Evaluation as E
import qualified Wasp.Analyzer.Evaluator.EvaluationError as EvaluationError
import Wasp.Analyzer.Parser (parse) import Wasp.Analyzer.Parser (parse)
import qualified Wasp.Analyzer.Type as T
import Wasp.Analyzer.TypeChecker (typeCheck) import Wasp.Analyzer.TypeChecker (typeCheck)
import qualified Wasp.Analyzer.TypeChecker.AST as TypedAST
import qualified Wasp.Analyzer.TypeDefinitions as TD import qualified Wasp.Analyzer.TypeDefinitions as TD
import Wasp.Analyzer.TypeDefinitions.Class.HasCustomEvaluation (HasCustomEvaluation (..))
import Wasp.Analyzer.TypeDefinitions.TH import Wasp.Analyzer.TypeDefinitions.TH
import Wasp.AppSpec.Core.Decl (IsDecl) import Wasp.AppSpec.Core.Decl (IsDecl)
import Wasp.AppSpec.Core.Ref (Ref (..)) import Wasp.AppSpec.Core.Ref (Ref (..))
@ -21,18 +29,24 @@ fromRight :: Show a => Either a b -> b
fromRight (Right x) = x fromRight (Right x) = x
fromRight (Left e) = error $ show e fromRight (Left e) = error $ show e
------- Simple -------
newtype Simple = Simple String deriving (Eq, Show, Data) newtype Simple = Simple String deriving (Eq, Show, Data)
instance IsDecl Simple instance IsDecl Simple
makeDeclType ''Simple makeDeclType ''Simple
------- Fields -------
data Fields = Fields {a :: String, b :: Maybe Double} deriving (Eq, Show, Data) data Fields = Fields {a :: String, b :: Maybe Double} deriving (Eq, Show, Data)
instance IsDecl Fields instance IsDecl Fields
makeDeclType ''Fields makeDeclType ''Fields
------ Business ------
data Person = Person {name :: String, age :: Integer} deriving (Eq, Show, Data) data Person = Person {name :: String, age :: Integer} deriving (Eq, Show, Data)
instance IsDecl Person instance IsDecl Person
@ -43,12 +57,6 @@ data BusinessType = Manufacturer | Seller | Store deriving (Eq, Show, Data)
makeEnumType ''BusinessType makeEnumType ''BusinessType
data Special = Special {imps :: [ExtImport], json :: JSON} deriving (Eq, Show)
instance IsDecl Special
makeDeclType ''Special
data Business = Business data Business = Business
{ employees :: [Ref Person], { employees :: [Ref Person],
worth :: Double, worth :: Double,
@ -61,6 +69,53 @@ instance IsDecl Business
makeDeclType ''Business makeDeclType ''Business
-------- Special --------
data Special = Special {imps :: [ExtImport], json :: JSON} deriving (Eq, Show)
instance IsDecl Special
makeDeclType ''Special
------ HasCustomEvaluation ------
data SemanticVersion = SemanticVersion Int Int Int
deriving (Eq, Show, Data)
instance HasCustomEvaluation SemanticVersion where
waspType = T.StringType
evaluation = E.evaluation' $ \case
TypedAST.StringLiteral str -> case splitOn "." str of
[major, minor, patch] ->
maybe
( Left $
EvaluationError.ParseError $
EvaluationError.EvaluationParseError
"Failed parsing semantic version -> some part is not int"
)
pure
$ do
majorInt <- readMaybe @Int major
minorInt <- readMaybe @Int minor
patchInt <- readMaybe @Int patch
return $ SemanticVersion majorInt minorInt patchInt
_ ->
Left $
EvaluationError.ParseError $
EvaluationError.EvaluationParseError $
"Failed parsing semantic version -> it doesn't have 3 comma separated parts."
expr -> Left $ EvaluationError.ExpectedType T.StringType (TypedAST.exprType expr)
data Custom = Custom
{version :: SemanticVersion}
deriving (Eq, Show, Data)
instance IsDecl Custom
makeDeclType ''Custom
--------------------------------
eval :: TD.TypeDefinitions -> [String] -> Either EvaluationError [Decl] eval :: TD.TypeDefinitions -> [String] -> Either EvaluationError [Decl]
eval typeDefs source = evaluate typeDefs $ fromRight $ typeCheck typeDefs $ fromRight $ parse $ unlines source eval typeDefs source = evaluate typeDefs $ fromRight $ typeCheck typeDefs $ fromRight $ parse $ unlines source
@ -112,6 +167,7 @@ spec_Evaluator = do
" json: {=json \"key\": 1 json=}", " json: {=json \"key\": 1 json=}",
"}" "}"
] ]
fmap takeDecls (eval typeDefs source) fmap takeDecls (eval typeDefs source)
`shouldBe` Right `shouldBe` Right
[ ( "Test", [ ( "Test",
@ -120,3 +176,9 @@ spec_Evaluator = do
(JSON " \"key\": 1 ") (JSON " \"key\": 1 ")
) )
] ]
it "Evaluates a declaration with a field that has custom evaluation" $ do
let typeDefs = TD.addDeclType @Custom $ TD.empty
let decls = eval typeDefs ["custom Test { version: \"1.2.3\" }"]
fmap takeDecls decls
`shouldBe` Right [("Test", Custom {version = SemanticVersion 1 2 3})]