From c4b13937d169dbca7ce99dc4b6c2f2a32403b7ca Mon Sep 17 00:00:00 2001 From: George Thomas Date: Tue, 24 Oct 2023 15:54:35 +0100 Subject: [PATCH] feat: Add support for creating simple 2D animations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have a new ADT, `Picture`, representing 2D images, and a primitive function `animate : Integer → (Integer → Picture) → Animation`, where `Animation` is a new primitive type, currently implemented as a base-64 encoding of a GIF for ease of use by clients. The arguments are a duration, in seconds, and a description of how to render each frame. This is inspired by the API of pure functional animation libraries such as Haskell's Gloss. We opt for keeping the API simple for now rather than providing full flexibility, so animations are always 10 frames/s on a low-resolution widescreen grid, looping forever (see `Values which are hardcoded`... comment). Both implementation and student-facing API are really just a proof-of-concept for exploring the use of Primer for effectful programming. Though much of what is added here (`Picture` ADT, new primitives etc.) is likely to remain useful for any future work in this direction. The one major current limitation is around the kinds of functions which we can pass as the second argument to `animate` without the evaluator getting stuck (`simple substitution hack`... comment). This is a high-priority task which will require some reworking of the evaluator, the details of which are as yet unclear. Using GIFs with `tasty-golden` for testing mostly works well, except that it has some issues displaying the `ByteString` in the terminal when the output differs from what is expected (outputs `Exception: Cannot decode byte`). But this is fine, since what we'd want to do in such a situation is to just pass `--accept` and compare the old and new GIF as rendered. Signed-off-by: George Thomas --- flake.nix | 2 + .../test/outputs/OpenAPI/openapi.json | 18 ++ primer/gen/Primer/Gen/Core/Raw.hs | 1 + primer/gen/Primer/Gen/Core/Typed.hs | 3 +- primer/primer.cabal | 4 + primer/src/Primer/Builtins/Picture.hs | 1 + primer/src/Primer/Core/Meta.hs | 2 + primer/src/Primer/Module.hs | 5 +- primer/src/Primer/Pretty.hs | 1 + primer/src/Primer/Primitives.hs | 188 +++++++++++++++++- primer/src/Primer/Primitives/PrimDef.hs | 1 + primer/src/Primer/Typecheck.hs | 4 +- primer/test/Tests/Action.hs | 6 +- primer/test/Tests/Action/Prog.hs | 14 +- .../Beginner-Editable.fragment | 24 +++ .../M.comprehensive/Expert-Editable.fragment | 168 ++++++++++++++++ .../Intermediate-Editable.fragment | 168 ++++++++++++++++ 17 files changed, 591 insertions(+), 19 deletions(-) create mode 100644 primer/src/Primer/Builtins/Picture.hs 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 b852a7a8c..334c1e321 100644 --- a/primer-service/test/outputs/OpenAPI/openapi.json +++ b/primer-service/test/outputs/OpenAPI/openapi.json @@ -628,6 +628,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 3137cc075..57027a175 100644 --- a/primer/gen/Primer/Gen/Core/Raw.hs +++ b/primer/gen/Primer/Gen/Core/Raw.hs @@ -165,6 +165,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 57e75b18c..783e2f486 100644 --- a/primer/gen/Primer/Gen/Core/Typed.hs +++ b/primer/gen/Primer/Gen/Core/Typed.hs @@ -487,7 +487,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) @@ -676,6 +676,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 06dd2fddd..590871b8d 100644 --- a/primer/primer.cabal +++ b/primer/primer.cabal @@ -23,6 +23,7 @@ library Primer.App.Utils Primer.Builtins Primer.Builtins.DSL + Primer.Builtins.Picture Primer.Core Primer.Core.DSL Primer.Core.Meta @@ -109,8 +110,11 @@ 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 diff --git a/primer/src/Primer/Builtins/Picture.hs b/primer/src/Primer/Builtins/Picture.hs new file mode 100644 index 000000000..065c2b837 --- /dev/null +++ b/primer/src/Primer/Builtins/Picture.hs @@ -0,0 +1 @@ +module Primer.Builtins.Picture () where 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..14b3ff686 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,66 @@ module Primer.Primitives ( primFunDef, PrimFunError (..), primitiveModuleName, + pictureDef, + tPicture, + cCircle, + cRect, + cColour, + cRotate, + cTranslate, + cCompoundPicture, ) where -import Foreword +import Foreword hiding (rotate) 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 ( + GifLooping (LoopingForever), + defaultPaletteOptions, + rasterGif, + ) +import Diagrams.Prelude ( + V2 (..), + circle, + deg, + fillColor, + mkP2, + mkSizeSpec, + rect, + rectEnvelope, + 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 +83,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 +110,7 @@ primConName :: PrimCon -> TyConName primConName = \case PrimChar _ -> tChar PrimInt _ -> tInt + PrimAnimation _ -> tAnimation primitive :: Name -> GlobalName k primitive = qualifyName primitiveModuleName @@ -84,6 +121,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 +147,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 +161,7 @@ allPrimTypeDefs = _ = \case PrimChar _ -> () PrimInt _ -> () + PrimAnimation _ -> () primDefName :: PrimDef -> Name primDefName = \case @@ -137,6 +185,7 @@ primDefName = \case IntNeq -> "Int.≠" IntToNat -> "Int.toNat" IntFromNat -> "Int.fromNat" + Animate -> "animate" PrimConst -> "const" primDefType :: PrimDef -> Type' () () @@ -164,12 +213,20 @@ primFunTypes = \case IntNeq -> ([c tInt, c tInt], c tBool) IntToNat -> ([c tInt], c tMaybe `a` c tNat) IntFromNat -> ([c tNat], c tInt) + Animate -> + ( + [ c tInt -- Loop time, in seconds. + , c tInt `f` c tPicture -- A function from frame number to output. + ] + , 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 +333,49 @@ primFunDef def args = case def of [exprToNat -> Just n] -> Right $ int $ fromIntegral n _ -> err + Animate -> case args of + -- Since we can only translate 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 <- traverse diagramAtTime [0 .. duration * 100 `div` frameLength] -> + 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) + $ rasterGif @Double + (mkSizeSpec $ Just . fromInteger <$> V2 width height) + gifLooping + defaultPaletteOptions + $ map + ( (,fromInteger frameLength) + . 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 +385,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) + Rect w h -> rect (fromInteger w) (fromInteger h) + 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 cRect [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" +cRect :: ValConName +cRect = 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 == cRect -> + 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 4c06767ff..441d86f09 100644 --- a/primer/src/Primer/Typecheck.hs +++ b/primer/src/Primer/Typecheck.hs @@ -164,7 +164,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,7 +744,7 @@ 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 + unless (tc == tInt || tc == tChar || tc == tAnimation) $ throwError' $ InternalError $ "Unknown primitive type: " <> show tc let f b = case caseBranchName b of PatCon _ -> Nothing PatPrim pc -> case pc 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 c9e3fa8f4..3037e4391 100644 --- a/primer/test/Tests/Action/Prog.hs +++ b/primer/test/Tests/Action/Prog.hs @@ -1197,7 +1197,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' @@ -1211,8 +1211,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")) @@ -1221,8 +1221,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 @@ -1237,7 +1237,7 @@ unit_ParamKindAction_3 = progActionTest ( defaultProgEditableTypeDefs (pure []) ) - [ ParamKindAction tT pA [SetCursor 29, Delete] + [ ParamKindAction tT pA [SetCursor 44, Delete] ] $ expectSuccess $ \_ prog' -> do @@ -1252,7 +1252,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/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 81ac37f9c..4036e344d 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 }