diff --git a/flake.nix b/flake.nix index 4da3cb91f..43dc0e957 100644 --- a/flake.nix +++ b/flake.nix @@ -415,6 +415,8 @@ # https://github.com/input-output-hk/haskell.nix/issues/1242 packages.mtl-compat.writeHieFiles = false; packages.bytestring-builder.writeHieFiles = false; + packages.fail.writeHieFiles = false; + packages.diagrams.writeHieFiles = false; } { #TODO This shouldn't be necessary - see the commented-out `build-tool-depends` in primer.cabal. diff --git a/primer-service/test/outputs/OpenAPI/openapi.json b/primer-service/test/outputs/OpenAPI/openapi.json index c626c9a1e..b213594ba 100644 --- a/primer-service/test/outputs/OpenAPI/openapi.json +++ b/primer-service/test/outputs/OpenAPI/openapi.json @@ -672,6 +672,24 @@ "contents" ], "type": "object" + }, + { + "properties": { + "contents": { + "type": "string" + }, + "tag": { + "enum": [ + "PrimAnimation" + ], + "type": "string" + } + }, + "required": [ + "tag", + "contents" + ], + "type": "object" } ] }, diff --git a/primer/gen/Primer/Gen/Core/Raw.hs b/primer/gen/Primer/Gen/Core/Raw.hs index 580a7b2c4..dfd1fd8dd 100644 --- a/primer/gen/Primer/Gen/Core/Raw.hs +++ b/primer/gen/Primer/Gen/Core/Raw.hs @@ -168,6 +168,7 @@ genPrimCon = _ = \case PrimChar _ -> () PrimInt _ -> () + PrimAnimation _ -> () genType :: ExprGen Type genType = diff --git a/primer/gen/Primer/Gen/Core/Typed.hs b/primer/gen/Primer/Gen/Core/Typed.hs index 55088c782..fa7cee4ce 100644 --- a/primer/gen/Primer/Gen/Core/Typed.hs +++ b/primer/gen/Primer/Gen/Core/Typed.hs @@ -490,7 +490,7 @@ genChk ty = do brs0 <- Gen.list (Range.linear 0 5) $ do p <- pg (p,) . CaseBranch (PatPrim p) [] <$> genChk ty - let brs = nubSortOn ((\case PrimInt n -> Left n; PrimChar c -> Right c) . fst) brs0 + let brs = nubSortOn ((\case PrimInt n -> Left (Left n); PrimChar c -> Left (Right c); PrimAnimation b -> Right b) . fst) brs0 fb <- genChk ty pure $ Case () e (snd <$> brs) (CaseFallback fb) @@ -679,6 +679,7 @@ genPrimCon = catMaybes <$> sequence [whenInScope PrimChar 'a' genChar, whenInSco _ = \case PrimChar _ -> () PrimInt _ -> () + PrimAnimation _ -> () -- We bias the distribution towards a small set, to make it more likely we -- generate name clashes on occasion diff --git a/primer/primer.cabal b/primer/primer.cabal index 5139c42ea..bdde7d40d 100644 --- a/primer/primer.cabal +++ b/primer/primer.cabal @@ -109,11 +109,15 @@ library , aeson >=2.0 && <2.2 , assoc ^>=1.1 , base >=4.12 && <4.19 + , base64-bytestring ^>=1.2.1 , containers >=0.6.0.1 && <0.7.0 , deriving-aeson >=0.2 && <0.3.0 + , diagrams-lib ^>=1.4.6 + , diagrams-rasterific ^>=1.4.2 , exceptions >=0.10.4 && <0.11.0 , extra >=1.7.10 && <1.8.0 , generic-optics >=2.0 && <2.3.0 + , JuicyPixels ^>=3.3.8 , list-t >=1.0 && <1.1.0 , logging-effect ^>=1.4 , mmorph ^>=1.2.0 @@ -267,6 +271,7 @@ test-suite primer-test , aeson , aeson-pretty ^>=0.8.9 , base + , base64-bytestring , bytestring , containers , extra diff --git a/primer/src/Primer/Core/Meta.hs b/primer/src/Primer/Core/Meta.hs index ced09b4e4..c2821cca4 100644 --- a/primer/src/Primer/Core/Meta.hs +++ b/primer/src/Primer/Core/Meta.hs @@ -195,6 +195,8 @@ instance HasMetadata (Meta a) where data PrimCon = PrimChar Char | PrimInt Integer + | -- | Contains a base-64 encoding of an animated GIF. + PrimAnimation Text deriving stock (Eq, Show, Read, Data, Generic) deriving (FromJSON, ToJSON) via PrimerJSON PrimCon deriving anyclass (NFData) diff --git a/primer/src/Primer/Module.hs b/primer/src/Primer/Module.hs index ee11fd19a..a8ff3cfcf 100644 --- a/primer/src/Primer/Module.hs +++ b/primer/src/Primer/Module.hs @@ -62,7 +62,7 @@ import Primer.JSON ( ToJSON, ) import Primer.Name (Name) -import Primer.Primitives (allPrimTypeDefs, primDefName, primitiveModuleName) +import Primer.Primitives (allPrimTypeDefs, pictureDef, primDefName, primitiveModuleName, tPicture) import Primer.TypeDef (TypeDef (..), TypeDefMap, forgetTypeDefMetadata, generateTypeDefIDs) data Module = Module @@ -133,10 +133,11 @@ nextModuleID m = primitiveModule :: MonadFresh ID m => m Module primitiveModule = do allPrimTypeDefs' <- traverse (generateTypeDefIDs . TypeDefPrim) allPrimTypeDefs + pictureDef' <- generateTypeDefIDs $ TypeDefAST pictureDef pure Module { moduleName = primitiveModuleName - , moduleTypes = M.mapKeys baseName allPrimTypeDefs' + , moduleTypes = M.mapKeys baseName allPrimTypeDefs' <> M.fromList [(baseName tPicture, pictureDef')] , moduleDefs = M.fromList $ [(primDefName def, DefPrim def) | def <- enumerate] } diff --git a/primer/src/Primer/Pretty.hs b/primer/src/Primer/Pretty.hs index 7ffdd2e32..82be3c693 100644 --- a/primer/src/Primer/Pretty.hs +++ b/primer/src/Primer/Pretty.hs @@ -185,6 +185,7 @@ prettyExpr opts = \case prim = \case PrimChar c -> "Char" <+> pretty @Text (show c) PrimInt n -> "Int" <+> pretty @Text (show n) + PrimAnimation n -> pretty @Text (show n) typeann e t = brac Round Yellow (pE e) <+> col Yellow "::" <> line <> brac Round Yellow (pT t) -- When grouped: " x " diff --git a/primer/src/Primer/Primitives.hs b/primer/src/Primer/Primitives.hs index e12ace782..6d592227d 100644 --- a/primer/src/Primer/Primitives.hs +++ b/primer/src/Primer/Primitives.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BlockArguments #-} {-# LANGUAGE ImpredicativeTypes #-} {-# LANGUAGE ViewPatterns #-} @@ -6,6 +7,7 @@ module Primer.Primitives ( allPrimTypeDefs, tInt, tChar, + tAnimation, primitive, primitiveGVar, primConName, @@ -14,33 +16,75 @@ module Primer.Primitives ( primFunDef, PrimFunError (..), primitiveModuleName, + pictureDef, + tPicture, + cCircle, + cRectangle, + cColour, + cRotate, + cTranslate, + cCompoundPicture, ) where -import Foreword +import Foreword hiding (rotate) +import Codec.Picture.ColorQuant (palettizeWithAlpha) +import Codec.Picture.Gif ( + GifDisposalMethod (DisposalRestoreBackground), + GifEncode (GifEncode), + GifLooping (LoopingForever), + encodeComplexGifImage, + ) import Control.Monad.Fresh (MonadFresh) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.ByteString.Base64 qualified as B64 import Data.Data (Data) import Data.Map qualified as M +import Diagrams.Backend.Rasterific ( + Options (RasterificOptions), + Rasterific (Rasterific), + ) +import Diagrams.Prelude ( + Diagram, + V2 (..), + circle, + deg, + fillColor, + lineWidth, + mkP2, + mkSizeSpec, + rect, + rectEnvelope, + renderDia, + rotate, + sRGB24, + translate, + (@@), + ) import Numeric.Natural (Natural) import Primer.Builtins ( + cCons, + cNil, cSucc, cZero, tBool, + tList, tMaybe, tNat, ) import Primer.Builtins.DSL (boolAnn, maybeAnn, nat) import Primer.Core ( Expr, - Expr' (Con, PrimCon), + Expr' (..), GVarName, GlobalName, ID, ModuleName, - PrimCon (PrimChar, PrimInt), + PrimCon (PrimAnimation, PrimChar, PrimInt), + TmVarRef (LocalVarRef), TyConName, Type' (..), + ValConName, mkSimpleModuleName, qualifyName, ) @@ -48,13 +92,14 @@ import Primer.Core.DSL ( ann, char, int, + prim, tcon, ) import Primer.Core.Utils (generateIDs) import Primer.JSON (CustomJSON (..), PrimerJSON) import Primer.Name (Name) import Primer.Primitives.PrimDef (PrimDef (..)) -import Primer.TypeDef (PrimTypeDef (..)) +import Primer.TypeDef (ASTTypeDef (..), PrimTypeDef (..), ValCon (..)) data PrimFunError = -- | We have attempted to apply a primitive function to invalid args. @@ -74,6 +119,7 @@ primConName :: PrimCon -> TyConName primConName = \case PrimChar _ -> tChar PrimInt _ -> tInt + PrimAnimation _ -> tAnimation primitive :: Name -> GlobalName k primitive = qualifyName primitiveModuleName @@ -84,6 +130,9 @@ tChar = primitive "Char" tInt :: TyConName tInt = primitive "Int" +tAnimation :: TyConName +tAnimation = primitive "Animation" + -- | Construct a reference to a primitive definition. primitiveGVar :: PrimDef -> GVarName primitiveGVar = primitive . primDefName @@ -107,6 +156,13 @@ allPrimTypeDefs = , primTypeDefNameHints = ["i", "j", "k", "m", "n"] } ) + , let name = tAnimation + in ( name + , PrimTypeDef + { primTypeDefParameters = [] + , primTypeDefNameHints = [] + } + ) ] where -- This ensures that when we modify the constructors of `PrimCon` (i.e. we add/remove primitive types), @@ -114,6 +170,7 @@ allPrimTypeDefs = _ = \case PrimChar _ -> () PrimInt _ -> () + PrimAnimation _ -> () primDefName :: PrimDef -> Name primDefName = \case @@ -137,6 +194,7 @@ primDefName = \case IntNeq -> "Int.≠" IntToNat -> "Int.toNat" IntFromNat -> "Int.fromNat" + Animate -> "animate" PrimConst -> "const" primDefType :: PrimDef -> Type' () () @@ -164,12 +222,25 @@ primFunTypes = \case IntNeq -> ([c tInt, c tInt], c tBool) IntToNat -> ([c tInt], c tMaybe `a` c tNat) IntFromNat -> ([c tNat], c tInt) + Animate -> + -- A loop time, in seconds, and a function from frame number to output. + -- Note that the number of frames per second is currently hardcoded to 10, and that + -- ideally we'd use floats here and the function would take a time in seconds as well. + -- Thus `Animate n p` will denote an animation of `10*n` frames of `0.1`s duration each, for + -- a total of `n` seconds, and will call `p` with arguments `0,1,...,10*n-1` to compute each frame. + ( + [ c tInt + , c tInt `f` c tPicture + ] + , c tAnimation + ) -- Arbitrarily limited to `Int` and `Bool` since we our system doesn't allow polymorphic primitives. -- Note that this primitive is only for testing anyway. PrimConst -> ([c tBool, c tNat], c tBool) where c = TCon () a = TApp () + f = TFun () primFunDef :: PrimDef -> [Expr' () () ()] -> Either PrimFunError (forall m. MonadFresh ID m => m Expr) primFunDef def args = case def of @@ -276,6 +347,51 @@ primFunDef def args = case def of [exprToNat -> Just n] -> Right $ int $ fromIntegral n _ -> err + Animate -> case args of + -- Since we only support translating a `Picture` expression to an image once it is in normal form, + -- this guard will only pass when `picture` has no free variables other than `time`. + [PrimCon () (PrimInt duration), Lam () time picture] + | Just (frames :: [Diagram Rasterific]) <- traverse diagramAtTime [0 .. (duration * 100) `div` frameLength - 1] -> + Right + $ prim + $ PrimAnimation + $ either + -- This case really shouldn't be able to happen, unless `diagrams-rasterific` is broken. + -- In fact, the default behaviour (`animatedGif`) is just to write the error to `stdout`, + -- and we only have to handle this because we need to use the lower-level `rasterGif`, + -- for unrelated reasons (getting the `Bytestring` without dumping it to a file). + mempty + (decodeUtf8 . B64.encode . toS) + $ encodeComplexGifImage + $ GifEncode (fromInteger width) (fromInteger height) Nothing Nothing gifLooping + $ flip palettizeWithAlpha DisposalRestoreBackground + $ map + ( (fromInteger frameLength,) + . renderDia + Rasterific + (RasterificOptions (mkSizeSpec $ Just . fromInteger <$> V2 width height)) + . rectEnvelope + (fromInteger <$> mkP2 (-width `div` 2) (-height `div` 2)) + (fromInteger <$> V2 width height) + ) + frames + where + -- Note that this simple substitution hack only allows for trivial functions, + -- i.e. those where only substitution is needed for the function body to reach a normal form. + -- Our primitives system doesn't yet support further evaluation here. + diagramAtTime t = exprToDiagram $ substTime (PrimCon () (PrimInt t)) picture + where + substTime a = \case + Var () (LocalVarRef t') | t' == time -> a + Con () c es -> Con () c $ map (substTime a) es + e -> e + -- Values which are hardcoded, for now at least, for the sake of keeping the student-facing API simple. + -- We keep the frame rate and resolution low to avoid serialising huge GIFs. + gifLooping = LoopingForever + frameLength = 10 -- in hundredths of a second, as per the GIF spec + width = 160 + height = 90 + _ -> err PrimConst -> case args of [x, _] -> Right $ generateIDs x `ann` tcon tBool @@ -285,4 +401,84 @@ primFunDef def args = case def of Con _ c [] | c == cZero -> Just 0 Con _ c [x] | c == cSucc -> succ <$> exprToNat x _ -> Nothing + exprToDiagram e = + exprToPicture e <&> fix \f -> \case + Circle r -> + if r == 0 -- `diagrams` crashes with a divide-by-zero if we don't catch this case + then mempty + else circle (fromInteger r) & lineWidth 0 + Rect w h -> rect (fromInteger w) (fromInteger h) & lineWidth 0 + Colour r g b p -> f p & fillColor (sRGB24 (fromInteger r) (fromInteger g) (fromInteger b)) + Rotate a p -> f p & rotate (fromInteger a @@ deg) + Translate x y p -> f p & translate (V2 (fromInteger x) (fromInteger y)) + CompoundPicture ps -> foldMap' f ps err = Left $ PrimFunError def args + +pictureDef :: ASTTypeDef () () +pictureDef = + ASTTypeDef + { astTypeDefParameters = [] + , astTypeDefConstructors = + [ ValCon cCircle [TCon () tInt] + , ValCon cRectangle [TCon () tInt, TCon () tInt] + , ValCon cColour [TCon () tInt, TCon () tInt, TCon () tInt, TCon () tPicture] + , ValCon cRotate [TCon () tInt, TCon () tPicture] + , ValCon cTranslate [TCon () tInt, TCon () tInt, TCon () tPicture] + , -- Pictures are ordered foreground to background, i.e. those earlier in the list appear on top. + ValCon cCompoundPicture [TApp () (TCon () tList) (TCon () tPicture)] + ] + , astTypeDefNameHints = [] + } + +tPicture :: TyConName +tPicture = primitive "Picture" +cCircle :: ValConName +cCircle = primitive "Circle" +cRectangle :: ValConName +cRectangle = primitive "Rectangle" +cColour :: ValConName +cColour = primitive "Colour" +cRotate :: ValConName +cRotate = primitive "Rotate" +cTranslate :: ValConName +cTranslate = primitive "Translate" +cCompoundPicture :: ValConName +cCompoundPicture = primitive "Compound" + +-- | A Haskell model of our built-in `Picture` type. +-- Using this type can make working with pictures more convenient, +-- including by giving us compile-time exhaustiveness checks. +data Picture + = Circle Integer + | Rect Integer Integer + | Colour Integer Integer Integer Picture + | Rotate Integer Picture + | Translate Integer Integer Picture + | CompoundPicture [Picture] + +exprToPicture :: Expr' a b c -> Maybe Picture +exprToPicture = \case + Con _ c [PrimCon _ (PrimInt r)] + | c == cCircle -> + Just $ Circle r + Con _ c [PrimCon _ (PrimInt w), PrimCon _ (PrimInt h)] + | c == cRectangle -> + Just $ Rect w h + Con _ c [PrimCon _ (PrimInt r), PrimCon _ (PrimInt g), PrimCon _ (PrimInt b), exprToPicture -> Just p] + | c == cColour -> + Just $ Colour r g b p + Con _ c [PrimCon _ (PrimInt a), exprToPicture -> Just p] + | c == cRotate -> + Just $ Rotate a p + Con _ c [PrimCon _ (PrimInt x), PrimCon _ (PrimInt y), exprToPicture -> Just p] + | c == cTranslate -> + Just $ Translate x y p + Con _ c [exprToList -> Just (traverse exprToPicture -> Just ps)] + | c == cCompoundPicture -> + Just $ CompoundPicture ps + _ -> Nothing + where + exprToList = \case + Con _ c [] | c == cNil -> Just [] + Con _ c [x, exprToList -> Just xs] | c == cCons -> Just $ x : xs + _ -> Nothing diff --git a/primer/src/Primer/Primitives/PrimDef.hs b/primer/src/Primer/Primitives/PrimDef.hs index f23bbaf29..1e6644a7c 100644 --- a/primer/src/Primer/Primitives/PrimDef.hs +++ b/primer/src/Primer/Primitives/PrimDef.hs @@ -34,6 +34,7 @@ data PrimDef | IntNeq | IntToNat | IntFromNat + | Animate | -- | Only for testing PrimConst deriving stock (Eq, Show, Read, Enum, Bounded, Data, Generic) diff --git a/primer/src/Primer/Typecheck.hs b/primer/src/Primer/Typecheck.hs index 45f92553f..0b401328e 100644 --- a/primer/src/Primer/Typecheck.hs +++ b/primer/src/Primer/Typecheck.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedLabels #-} -- | Typechecking for Core expressions. @@ -164,7 +165,7 @@ import Primer.Module ( moduleTypesQualifiedMeta, ) import Primer.Name (Name, NameCounter) -import Primer.Primitives (primConName, tChar, tInt) +import Primer.Primitives (primConName, tAnimation, tChar, tInt) import Primer.Subst (substTy) import Primer.TypeDef ( ASTTypeDef (astTypeDefConstructors, astTypeDefParameters), @@ -744,20 +745,25 @@ check t = \case scrutWrap <- Hole <$> meta' (TCSynthed (TEmptyHole ())) <*> pure (addChkMetaT (TEmptyHole ()) e') pure $ Case caseMeta scrutWrap [] CaseExhaustive Left (TDIPrim tc) -> do - unless (tc == tInt || tc == tChar) $ throwError' $ InternalError $ "Unknown primitive type: " <> show tc - let f b = case caseBranchName b of - PatCon _ -> Nothing - PatPrim pc -> case pc of - PrimInt p | tc == tInt -> Just $ Left (p, b) - PrimChar p | tc == tChar -> Just $ Right (p, b) - _ -> Nothing -- all branches right sort & order sh <- asks smartHoles - brs' <- case partitionEithers <$> traverse f brs of - Just ([], chs) | isSorted (fst <$> chs) -> pure $ snd <$> chs - Just (is, []) | isSorted (fst <$> is) -> pure $ snd <$> is - _ | NoSmartHoles <- sh -> throwError' $ WrongCaseBranches tc (caseBranchName <$> brs) (fb /= CaseExhaustive) - _ | SmartHoles <- sh -> pure [] + consistentBranches <- + if + | tc == tInt -> pure $ maybe False isSorted $ for (map caseBranchName brs) $ \case + PatPrim (PrimInt p) -> pure p + _ -> Nothing + | tc == tChar -> pure $ maybe False isSorted $ for (map caseBranchName brs) $ \case + PatPrim (PrimChar p) -> pure p + _ -> Nothing + -- some primitives do not admit any sensible notion of pattern matching + | tc == tAnimation -> pure $ null brs + | otherwise -> throwError' $ InternalError $ "Unknown primitive type: " <> show tc + brs' <- + if consistentBranches + then pure brs + else case sh of + NoSmartHoles -> throwError' $ WrongCaseBranches tc (caseBranchName <$> brs) (fb /= CaseExhaustive) + SmartHoles -> pure [] -- no params, check the rhs brs'' <- for brs' $ \(CaseBranch c ps rhs) -> do case (ps, sh) of diff --git a/primer/test/Tests/Action.hs b/primer/test/Tests/Action.hs index 086f260e4..521e1542b 100644 --- a/primer/test/Tests/Action.hs +++ b/primer/test/Tests/Action.hs @@ -205,7 +205,7 @@ unit_6 = ) [Move Child1, Move Child1, Move Child2, ConstructLam Nothing] ( ann - (lam "f" (app (lvar "f") (lam "a27" emptyHole))) + (lam "f" (app (lvar "f") (lam "a42" emptyHole))) (tfun (tfun tEmptyHole tEmptyHole) tEmptyHole) ) @@ -722,7 +722,7 @@ unit_case_on_hole = ( lam "x" $ case_ (ann emptyHole $ tcon tNat) - [branch cZero [] emptyHole, branch cSucc [("a29", Nothing)] emptyHole] -- NB: fragile names here + [branch cZero [] emptyHole, branch cSucc [("a44", Nothing)] emptyHole] -- NB: fragile names here ) (tfun (tcon tNat) (tcon tNat)) ) @@ -898,7 +898,7 @@ unit_rename_case_bind_clash = unit_case_branches :: Assertion unit_case_branches = let e cse = ann cse (tcon tBool) - n = "a24" + n = "a39" e0 = e $ caseFB_ diff --git a/primer/test/Tests/Action/Prog.hs b/primer/test/Tests/Action/Prog.hs index 5405f1888..e891f52cc 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -1199,7 +1199,7 @@ unit_ParamKindAction_1 = progActionTest ( defaultProgEditableTypeDefs (pure []) ) - [ParamKindAction tT pB [SetCursor 30, ConstructKFun]] + [ParamKindAction tT pB [SetCursor 45, ConstructKFun]] $ expectSuccess $ \_ prog' -> do td <- findTypeDef tT prog' @@ -1213,8 +1213,8 @@ unit_ParamKindAction_2 = progActionTest ( defaultProgEditableTypeDefs (pure []) ) - [ ParamKindAction tT pB [SetCursor 30, ConstructKFun] - , ParamKindAction tT pB [SetCursor 36, ConstructKType] + [ ParamKindAction tT pB [SetCursor 45, ConstructKFun] + , ParamKindAction tT pB [SetCursor 51, ConstructKType] ] $ expectError (@?= ActionError (CustomFailure ConstructKType "can only construct the kind 'Type' in hole")) @@ -1223,8 +1223,8 @@ unit_ParamKindAction_2b = progActionTest ( defaultProgEditableTypeDefs (pure []) ) - [ ParamKindAction tT pB [SetCursor 30, ConstructKFun] - , ParamKindAction tT pB [SetCursor 36, Delete] + [ ParamKindAction tT pB [SetCursor 45, ConstructKFun] + , ParamKindAction tT pB [SetCursor 51, Delete] ] $ expectSuccess $ \_ prog' -> do @@ -1239,7 +1239,7 @@ unit_ParamKindAction_3 = progActionTest ( defaultProgEditableTypeDefs (pure []) ) - [ ParamKindAction tT pA [SetCursor 29, Delete] + [ ParamKindAction tT pA [SetCursor 44, Delete] ] $ expectSuccess $ \_ prog' -> do @@ -1254,7 +1254,7 @@ unit_ParamKindAction_bad_id = progActionTest ( defaultProgEditableTypeDefs (pure []) ) - [ ParamKindAction tT pB [SetCursor 30, ConstructKFun] + [ ParamKindAction tT pB [SetCursor 45, ConstructKFun] , ParamKindAction tT pB [SetCursor 0, ConstructKType] ] $ expectError (@?= ActionError (IDNotFound 0)) diff --git a/primer/test/Tests/EvalFull.hs b/primer/test/Tests/EvalFull.hs index 7a51080e1..2767f7dd8 100644 --- a/primer/test/Tests/EvalFull.hs +++ b/primer/test/Tests/EvalFull.hs @@ -1,7 +1,10 @@ +{-# LANGUAGE ViewPatterns #-} + module Tests.EvalFull where import Foreword hiding (unlines) +import Data.ByteString.Base64 qualified as B64 import Data.List ((\\)) import Data.List.NonEmpty qualified as NE import Data.Map qualified as M @@ -60,10 +63,12 @@ import Primer.Module ( builtinModule, builtinTypes, moduleDefsQualified, + moduleTypesQualified, primitiveModule, ) import Primer.Primitives ( PrimDef ( + Animate, EqChar, HexToNat, IntAdd, @@ -86,6 +91,12 @@ import Primer.Primitives ( PrimConst, ToUpper ), + cCircle, + cColour, + cCompoundPicture, + cRectangle, + cRotate, + cTranslate, tChar, tInt, ) @@ -121,11 +132,14 @@ import Tasty ( withDiscards, withTests, ) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.Golden (goldenVsString) import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, (@?=)) import Tests.Action.Prog (readerToState) import Tests.Eval.Utils (genDirTm, hasHoles, hasTypeLets, testModules, (~=)) import Tests.Gen.Core.Typed (checkTest) import Tests.Typecheck (runTypecheckTestM, runTypecheckTestMWithPrims) +import Prelude (error) unit_1 :: Assertion unit_1 = @@ -1890,6 +1904,92 @@ unit_case_prim = s4 <- evalFullTest maxID4 mempty mempty 6 Syn e4 s4 <~==> Right expect4 +test_animation :: TestTree +test_animation = + testGroup + "animation" + $ zip + [(1 :: Int) ..] + [ pfun Animate + `app` int 1 + `app` lam + "t" + ( con + cColour + [ int 0 + , int 255 + , int 0 + , con1 cCircle (int 30) + ] + ) + , pfun Animate + `app` int 5 + `app` lam + "t" + ( con1 + cCompoundPicture + $ list_ + [ con + cColour + [ int 80 + , int 180 + , int 230 + , con + cTranslate + [ int (-35) + , int 0 + , con1 cCompoundPicture + $ list_ + [ con + cTranslate + [ int 7 + , int 7 + , con + cRotate + [ int (-45) + , con + cTranslate + [ int 0 + , int (-25) + , con cRectangle [int 20, int 50] + ] + ] + ] + , con + cRotate + [ int 45 + , con cRectangle [int 20, int 80] + ] + ] + ] + ] + , con + cColour + [ int 180 + , int 0 + , int 0 + , con + cTranslate + [ int 35 + , int 0 + , con1 cCircle $ lvar "t" + ] + ] + ] + ) + ] + <&> \(n, expr) -> + goldenVsString (show n) ("test/outputs/eval/animation/" <> show n <> ".gif") + $ evalFullTest 0 types defs 10 Syn (create' expr) + <&> \case + Right (PrimCon _ (PrimAnimation (B64.decode . encodeUtf8 -> Right t))) -> toS t + e -> error $ show e + where + builtins = create' builtinModule + prims = create' primitiveModule + types = moduleTypesQualified builtins <> moduleTypesQualified prims + defs = moduleDefsQualified builtins <> moduleDefsQualified prims + -- * Utilities evalFullTest' :: diff --git a/primer/test/outputs/available-actions/M.comprehensive/Beginner-Editable.fragment b/primer/test/outputs/available-actions/M.comprehensive/Beginner-Editable.fragment index b03355fcd..b3c809a26 100644 --- a/primer/test/outputs/available-actions/M.comprehensive/Beginner-Editable.fragment +++ b/primer/test/outputs/available-actions/M.comprehensive/Beginner-Editable.fragment @@ -1397,6 +1397,12 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Animation" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } , Option { option = "Char" , context = Just @@ -1409,6 +1415,12 @@ Output ( "Primitives" :| [] ) , matchesType = False } + , Option + { option = "Picture" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -1681,6 +1693,12 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Animation" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } , Option { option = "Char" , context = Just @@ -1693,6 +1711,12 @@ Output ( "Primitives" :| [] ) , matchesType = False } + , Option + { option = "Picture" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } diff --git a/primer/test/outputs/available-actions/M.comprehensive/Expert-Editable.fragment b/primer/test/outputs/available-actions/M.comprehensive/Expert-Editable.fragment index cf5aa2c64..08d07ad34 100644 --- a/primer/test/outputs/available-actions/M.comprehensive/Expert-Editable.fragment +++ b/primer/test/outputs/available-actions/M.comprehensive/Expert-Editable.fragment @@ -613,6 +613,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -833,6 +869,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -2157,6 +2229,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -2497,6 +2605,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -2739,6 +2883,12 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Animation" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } , Option { option = "Char" , context = Just @@ -2751,6 +2901,12 @@ Output ( "Primitives" :| [] ) , matchesType = False } + , Option + { option = "Picture" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -3722,6 +3878,12 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Animation" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } , Option { option = "Char" , context = Just @@ -3734,6 +3896,12 @@ Output ( "Primitives" :| [] ) , matchesType = False } + , Option + { option = "Picture" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } diff --git a/primer/test/outputs/available-actions/M.comprehensive/Intermediate-Editable.fragment b/primer/test/outputs/available-actions/M.comprehensive/Intermediate-Editable.fragment index bd4bf0e0b..00c0efd6d 100644 --- a/primer/test/outputs/available-actions/M.comprehensive/Intermediate-Editable.fragment +++ b/primer/test/outputs/available-actions/M.comprehensive/Intermediate-Editable.fragment @@ -421,6 +421,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -617,6 +653,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -1465,6 +1537,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -1733,6 +1841,42 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Circle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rectangle" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Colour" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Rotate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Translate" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } + , Option + { option = "Compound" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -1876,6 +2020,12 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Animation" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } , Option { option = "Char" , context = Just @@ -1888,6 +2038,12 @@ Output ( "Primitives" :| [] ) , matchesType = False } + , Option + { option = "Picture" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } @@ -2160,6 +2316,12 @@ Output ( "Builtins" :| [] ) , matchesType = False } + , Option + { option = "Animation" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } , Option { option = "Char" , context = Just @@ -2172,6 +2334,12 @@ Output ( "Primitives" :| [] ) , matchesType = False } + , Option + { option = "Picture" + , context = Just + ( "Primitives" :| [] ) + , matchesType = False + } ] , free = FreeNone } diff --git a/primer/test/outputs/eval/animation/1.gif b/primer/test/outputs/eval/animation/1.gif new file mode 100644 index 000000000..534df5ee5 Binary files /dev/null and b/primer/test/outputs/eval/animation/1.gif differ diff --git a/primer/test/outputs/eval/animation/2.gif b/primer/test/outputs/eval/animation/2.gif new file mode 100644 index 000000000..e92378997 Binary files /dev/null and b/primer/test/outputs/eval/animation/2.gif differ