From 76d51cc10fd6ee12568e809b311803e6cddcc413 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Wed, 1 Nov 2023 14:53:30 +0000 Subject: [PATCH 1/3] 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`, which seems to be down to https://github.com/UnkindPartition/tasty-golden/issues/51). 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/Core/Meta.hs | 2 + primer/src/Primer/Module.hs | 5 +- primer/src/Primer/Pretty.hs | 1 + primer/src/Primer/Primitives.hs | 194 +++++++++++++++++- 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 +- primer/test/Tests/EvalFull.hs | 100 +++++++++ .../Beginner-Editable.fragment | 24 +++ .../M.comprehensive/Expert-Editable.fragment | 168 +++++++++++++++ .../Intermediate-Editable.fragment | 168 +++++++++++++++ primer/test/outputs/eval/animation/1.gif | Bin 0 -> 3199 bytes primer/test/outputs/eval/animation/2.gif | Bin 0 -> 52327 bytes 19 files changed, 696 insertions(+), 19 deletions(-) create mode 100644 primer/test/outputs/eval/animation/1.gif create mode 100644 primer/test/outputs/eval/animation/2.gif 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 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..2eb3a2e87 100644 --- a/primer/primer.cabal +++ b/primer/primer.cabal @@ -109,8 +109,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 @@ -267,6 +270,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..0a54ba03d 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,67 @@ module Primer.Primitives ( primFunDef, PrimFunError (..), primitiveModuleName, + pictureDef, + tPicture, + cCircle, + cRectangle, + 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, + lineWidth, + 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 +84,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 +111,7 @@ primConName :: PrimCon -> TyConName primConName = \case PrimChar _ -> tChar PrimInt _ -> tInt + PrimAnimation _ -> tAnimation primitive :: Name -> GlobalName k primitive = qualifyName primitiveModuleName @@ -84,6 +122,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 +148,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 +162,7 @@ allPrimTypeDefs = _ = \case PrimChar _ -> () PrimInt _ -> () + PrimAnimation _ -> () primDefName :: PrimDef -> Name primDefName = \case @@ -137,6 +186,7 @@ primDefName = \case IntNeq -> "Int.≠" IntToNat -> "Int.toNat" IntFromNat -> "Int.fromNat" + Animate -> "animate" PrimConst -> "const" primDefType :: PrimDef -> Type' () () @@ -164,12 +214,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 +339,49 @@ 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 <- 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) + $ 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 +391,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..0ee262698 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 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 0000000000000000000000000000000000000000..37a307b8a0cb3b9160a37c0730759e466ccf024f GIT binary patch literal 3199 zcmZ?wbhEHbT)+^;@PPpg{xc~4=k{|A33hf2a5d61U}gjgEB<6*-~!S*ASEEh3{3x9 z`d6NQ%fEQemRsGM@9p``-}cC3+Oy7OuTE`y$9?>h&-HJ;&wri!{*RxBtNqBLj~zbW zn9eNwEE(Xn@Jd#Q*Osi65f8qde*5aclMOq<{Or#gZ>>ICw)gp2i|)StxBP$pxp(FI zcXy!|<`O{~6)z@A?6`4p#g7?+FD*I)H7h+QX704Sxbo*ufmfEV0<@}jO~~4D>cXmD zSpu)Ex&yk)S5NTXKKp|8_ipJoW;FphH9Qm6Y&&x&=9`S}ULRkx+P_oRu047;_A4)+ zs@=OUJh^p!>-fU+`uuLMJ8mc6zoxFAf4x5cz5VN@4>Vk16VlF@kg%zRUEC<=#%sm( z2||%iSKU}3>K3HF(9CVclZlg8PFPSg@ydi}OHNJIiQZMiX}s*rOq1+edpeDmpPQ=# o47Aa(8x6bBbT^u3N6Wj>x?;4h7_DbV+bE;$yV15Wq-|^s0PCo7mjD0& literal 0 HcmV?d00001 diff --git a/primer/test/outputs/eval/animation/2.gif b/primer/test/outputs/eval/animation/2.gif new file mode 100644 index 0000000000000000000000000000000000000000..508342fb4e16e6b1db4de10667c390dca025ee42 GIT binary patch literal 52327 zcmeFZXH=65w=JAbkd}}Dp(k_@l-@)Qy$BeJ2#5&Mo65F6?EUV2&b{aT@r^q^_j~R+=lKC+gfSQl#$0pGHP>2?zM-C~nj;lt z3)%+lKgS>M8MI8A)W0*SUovf4Fl|~q*)V^yap7d+;>o7@lT8aJflr#3Nev66hDB22 z0;zG4)U-fqS|qism^Ch$H7%MqESNXWovM6(s&dcu$=>zrd%yqU{V#+)CR1HY3vEr3 zuKXccD0u&)uQ1R7;Jftx&-ecf5PT1hPjvRkbFZxRUVG##{VdSuZE)?!u#bB*s;_}G z&#nGq{{e6He0q7HQ4K_rw~A9eQ%Ci6T?1jo6!K0H9U*%Xzq)(UEa>vcrymKVdna+B z6a}tpLCnG7aZ*s9s0--$WO1&9#Gu%ecvxB@Q)Y5tPHslNAMR@5wY(C4mOB+uG1VTJ z^1Av)N9?_OZMMAV_PR$_e4Q2S-KW5)zMQH-i>Eo{5pyu}SlUaAiOFloDKl`#+aUNm zv&CxzE2dzkwF|Q!OgBHBegFBy#_reeChfCD46=;M*D**=op9M%Em4-wM-|Tx<#fmC zd|>Gg)}d-d@kJ@ZxY_%wE{UAj`zA}4EivNfF|F0!X<%Tu^_$(xoa6#KZG|I#z8hZ~ zPc%~%6j3shaGp^v9!2>}0h=oaGA$^!XA2Y$meoiqifkAqV8;ubb@|~;M53)V(-XwQ zubdxjDlX*@$W#>A*xR4<%hINeo{N0Q_TZZn6%4ZS@lx|I-+ez68{HtnRGU-Ky?Bui zep89L7iJ%TLIted@YQs}rarziE#9)QhGh|Uo;dB;q+uY#V=l2wyW0l4XA{P0wKyA5 z(kCS1=C{Ecj=-UF7L|B=gRI$QBNb<|xu9UhZ~V8Z#0AQy3eLd%%{T0ntY;TBGmZ;` z4#T)$F5tjxvMo?b5MGvc&Vr5R#^8Iz(#?=;^>8T`Hny^n3EQE#&^(wo$|A0>H}$1C z>4@Tm*m^}prsIS(X1HL>ESl$K#yeEQp^SJpm@Q`6S_{s7puWRbORQo(#aMY|{x(TZ zav{~S76ti_n2Q9k_bY(kxDV?6=YKH0k7?0VR(>N7)i&0%92xnl!P`sbE(y#FJXxC+ z5!$4Jvz6$Yf?0{tL-e#>I(4%m`-b@$dXu`M*Nsv;wUDy~1Wjwt;@)`KP^(n)W+7Np zR5QNDGB;qQbAhDvQQ?88lH%6T(3)ug()!yC&@RFHIoK99@ z8toj^2%FiqhAm06Uel(7gds26eY-$xCm_*Di0G~Jc(UGOCw6SP2KCqr$fiR786jfo zHyPyaFbZ@SX@|c9fAJMP;l|21Nuh!4kCO8Fd)`xXMG=X*`NHfDWl#_H4Tf}ZwH%7d z3T+S;v=hSnT0+D>)!At+)vIT_J7l@j7Ibg@u)UCG7qqgF?hq}xn4t3%kGYqu|qGk7U>S3J-OU%z1-=`0N(55Lerw+l@YI+b0!}Fct50M?r&n*d{D~E z9s}9!v?Z$?HtUGk15m)Z+1P2Tq!{qzgr8Xe(i0%{ud`?N@lL85KKvKnSB5GTQpx*x zzn|Ta;SS*4?LrFr5Gq0Fv&x(A%abB`e~|jjpLjP0@UF%*xsP``fcKAo;(fY3sf+vv z-b??)y9DYb_*+Q>IZT1>#&{L%lMEmp2_PN-d{R1m;VXKQm3trX@kdEi{ul45d7_+B z2oNRFfSkwLWJsgN3*l(IQ1q%S;Rs|@6#}smO%i5%EgG1AOGHDK8}^f>-bv;?!3Hob zfo|RQ7=sWk_~8Tf;2_(5m^-I=EM~fwRxDOHO7` zREOU(5@`GIG*dsp$>}Ayj#Mor+ZN5D`$|=FoC{uSZgun@=rNBh*+!K#iwTZ+-WcpLk1?!j0{d^EaYy$Ww9P|V39i4CMp z*g@8$!w7q3MmPyDXZ}o5-76DXiMZr5SO>F2u|2vUm{7Pd;HvJmeBT(Lsu7 zLnY#R8VZ$h`vkPb)Lo zFwRklaB#0&T>&e9Ll^ayFuv{-MEeuTUJEQ_*a^+i^qoQJzVy#e6;(~5h3GLWgVV(= zlZsHhvHAewTUHjceS0OBaspp^xS{_}U-Jjs|2OpP%wru9`fU6{jHzY5Mt!#gwD+Vu zt=R_+QzeBT+P)fPG|ot2##ax_iwzp^xYlZO^(^V@^67n>%j_=i)8QUeQk)wtH#*8! zZ2I1Ocwp$(=`DBq{br^nv2$I6Bh>op>>D^Q>!XnH3HX3a4giQ^(H1hS@gvdsUj9Pt4q(y{hw# z+T4Qf4;TI5yGks*-7WzrQo!6zUrjG;(_@aeuP!fb{i9cH{YS4#ZES-k}K;wXh7340Y0V?31c~8n0B{WEMMkE@@=CeLF^espb#`*F=`BrBLM_3?+ z5QwcPNrY`tRD_x$szK+5AzA&LWp>Rx0RE@C!5lHcaa!;rK+_uR`tRYtlpAdHck+KF z2=w32@ksF>3*Mz}YRBm8AUr4f>qYe0;$VA&7C6zOfpkH*lJnR3l3})Z`B>sHbam5fDWJKH{tm1C6iRjz7Uj_TI7VPdM`|t@LE%Cco0N{S+~#tGQ-oaec3$bZj>0u9u0s@B>G zNi9CfvGA??{&iw;kSFWTBFFtm%K&8Lw;R6S93A(~5U4sq{`{XE?#EZ13auAnt!eKj z9E_8c848r|Jx6ja>!E*~*}fVdbK&HOtXA4pj=Z6W=Y@o*a8}Fg6S8^mPfjiZ$K0<- zYhC4JxEeu(Mi6BA55Mj3#b-;iJ8WctSN|x8h=04dlXev`kiWcbfUpVKI@JlzA#AyC(92NUriD^f|Iv_ zK%7KTqHLc;=c#wZG^jixjaogNYLG@td=kUvPzPAv0cbW zL@$xWQN3VbOS|y&TA+O9#8`w76d5J8VyKXepsvx6I^H)&F{J;NDnJ+{xdL|4{ z6$`LB_5@Y5=!l)l4pA&V^%|bVUMuymcf=v3miLwcaa1Ieq4I6Lu|#xJaKOi@t{{PF zz$(crnWvnkHcQ9*xN<>MyKKo{x8kY_^|S3e3NEg?m7Ml+|C04;6=OY2Ag&(PT95HZ z%hN%$I^}2iYQBG1rGWs2XC$p}_ntwdb^xo?tcHBtHVjxL)%bul?I1hS*5bz8Y)3aTWYM7h&PvUM7T*ae6Kn#EMle7v5A}T`o#mWdA4aLKI+Weo@BxN4xO&_cZ9&eL?II$^Rk$x4k=F z9W?Yl^zlqxu<@bnXUk2Y!xtXB-DnLny&ECP`#|6rFdMi+@nm7YiUEhmIpdJg7*u@n zxdTy&S7TE$Ps8q{VVJWDEV%O0m@^71jGzf6iRj9DeXg3?wDN{ZSW7KaTYYN#!`8>i zxZKXXM?EPl%>yN6PvS8B^pP;!-4HWEO_k+kRb6hD8m-Il6AZra(P)(RyrHPTH9p)#%At+dR-&SW^fA;GqzTkt9yBB3#u zj$V53rJo1|jeWbOu!P!vn@^A_sd@Y1m^S*`UItU$PyVFjC4?qQ;P{JzCUS^D)6T8y zydJc-21v0Hb6AMCWk&>4$EeN;Z0h`D@+E{Qih$XTvxvRGW*KA(6PEWIJ&siL<%59y zyYN^gEnzTXjGH{G#Ec~B>Mt2ki9sN~jUrj5DOKIX78JP{C20?(+_r$xSRh=+YLRfM zQ1fX{N84~9nPuOkrQ8kNER;E0Y8aVPYd|mL+%?3PNrd>osEvODk6^t6NrAtLl2C)~cIa zF|hv_TK%e;tp8K8{--AYf2zq3;8{Qw(*Sp!*` zR6lj*v)>BJif;%n)j6v_?=m(N60HopeY=vxmgRZDW$eX4KW(oxXs{Kshde*W0__(&GQPIiY zu0;#|{3)CVYA;}jMUQjCD4{=sV@4q#365`%Gh8UvqzLO~=$N4A>j%x?xI?fL7$q}O z3iK^^DmmI1CSgjTA0=k1qU-v*4fpVL&}F1ZzeyBn7nuOV#WIk=1p6*B$TBc65q1D4 z*$p;4&&y1gO#+z8Ac}PIzUgde5vAiAK#Ds1rc&&h8s1{gAnZ2T5 z9v!a6k|3J%$yGju`g=gZ!Eyf~L)h0MiQ~B%KkG5?IKBLpuxqjY9v%GH!*m?@9 z(}ejR)m#3M+oF%)0oA>{oYRhCHQ4*fonn!774*C!T@$^?y`2wwSOL`NBsUCvI%jz%q#8WA1w~vyDFOA|VO7it^bUf7A(@{*@LMRjlNfxC`a7hma z78e_6pafv1iJR_9NBy$^1#NXlIA6`UuLF;WuGfsO{deWLx^|al#Pv5S=pPA43kcNq zOA8bP`Y&Cg26**goM8hr*5Ef;^C?UU5TT31XSY-xU2@rk&aJL*XrhPrvVx8+^BFaW zD`oM_HHoM{FBbAb+CJtM(Pwp@{Z_-<-UM4k&rDBw z;#N&&tB!()`vcT0PrJ6L-|p(j2OLdZ*hOM@jAYw=h{Mles`cbU1t;)jgJO#R!m*7( zGFhXcN`S@s_>mNKvBt^a@ZHKFZ&weyvMcJ(7BA^Z*cX*y)=TxJnN1rspe6!`*5$ch zo0z;p)A%OC-#Jombw70Ty|l(#=4D+G;M^a@3RMh}U z2Fv}Ss@p{ns{?+-v`%Tz0@i#|ir3kbL|m_H&p)MT4u6=B|NhtYKN{$-O(z13%r|YAo-g ziYRred2E-(q=i>=TdqdCTI360&qgvbM82q!IvN=$xy>?OnjH8cFyLi*u^UrNp~o4; z+mr^c<=;|^!5_3DJ-5Hz?4A{!ZVRV~4U$MT1&VPT4DaaUe)*xULf5gy1r0#S_6H|O zDHwJD;=?m#a_ zA^L+^pb(NAd(r_rwQjKOfktAgg)v}+&$G+TE1{_WFv8io*2H4*?>_ZqqDW?P8JaEi zs}Yysr2i6CFKHznrti5~1oM^;jRf<^ozIUBKNj3?U_Kv1L>;}x3_I^UkS?J7VRI?@d@k#ubDisT4Eck5jLHj5 z&kZe$jgKCuj1JgfOR2^9w~D!;@<~b&X0L&T(@|(&zUC|Id86%R!>3ge8DW$ark0M2J;!Cr{E&-Yh!YkmaFEjv0!fKl%IfgW7GM z1XwZ@b|@;Y(k_jGih)=kNw16=--jO#`jr9Hgqkutd8?tU*XTU&C5 zMTlM{AO7XL-g+rJlm|r2KK2Y83}ZO`bE^+!Xjnu=$Nt1>B6R27=#jce*>$_?WL&|t z7V3vMJ?yv)3D3^*8%BuC6J|XYZz#f{DFCv<&uP^uCS&0kbz;6IBfUQ|dI{S}0I@{^ zQInqh>=+O=)fJ>=vgML(a4N5i{5Q)S}YcQFK?d31F0S z{xHf@9+7aF~Ctpp<$l0;@aNf zSujY3C`pP-8^TdqX{13Egt;gBc`8|r-Uo~_)&miM*`3sZXUErrlX{ic)*I=12j4e6 zxAJ=5Jbs?{Z#2q(74VZ^TE72p<1`~=Zx=)eXG z&j8U*DZ)=`bmKBXOEK=E*gJgp9shgbzT;GbVgs>W9;fo zoFi{&xd4lY;}LdVy-R0b2J)=^rcoNCbEGzAi$_6e>%~jP)G^_gx+^oKfd(hIqIsC# z-@iDA9B1_bUpk~bWL~YE8TM7~W%~L2F2cROBIiFQjP=;hk6qavi7Bsu6&McrTww~tkJAs7+bLbQi1h)ybiST1;H4w} z@Y3XEG-u$-K;#)~PryqX$9F@?k4B?1+6`7E*{|Oh0%zFT*$fwyi8&H)GAma06ftjd zSFs~g)5tY;25Ml4?}z){P>7cA60X(CqO{%^5av6s?keCWQ%hPUj#oAA{k@s!gCAPw z)4V>kEtgioKioHgf&Z?Q3IqZUH~uQ_Us~J$*V3&1w+wdp|33_NvQ&e2H^7nFDCVQS z<4$x7bzKziv8m-G0`dr$(=rfCqH85e2wvNu>d*Hd<9hE)&u9?qjtkU!6e)ST6lj43 zeMdKU8s_=(k5j@$)JG-w*bIxp9X#CJ1j~HzMkbS_TQ59)lP;Kib-bk%A@}MD-_R*{ z{&d~fM3?N;N`X?2`aJZBbf*d8xzA~{uTQ%ax`n*JnpXCcPj#H<+%ql)`{_oyiM+-> z2Vb;P$nruAk~XRc3XxoUGN&g4^Fy7?x$I5!wY^?lor%ii{Z{Zsjc4XvQ0kYV$!V4M z)&kdTZ+f>h&5gT0J@_f0;w(j+YI+ODHJx(3YsEMFf%y2j+P#~<>5)U+ncBV7rd5`6 zo_?H-DLDS}Q4VY8!cJnCZBArvFt7ucNS?A5&(^;b{DlpY_#O+UY{|@9=v**7y*GWF z*&Rgl|3($3v-9VCekQ0LCORBvG~?!o&^6d)yJ?85%Z(!A&{R#7L;tOCD;c~HyVP%E z!igaFIwv!IvCUJk5Lug!+Z-LE-KVj#l95m*o>XG0@i-7g+p})cVy1M6VwhcABoSD* zr_gRqNi)LpfUHY1R~JWK*q;_fnF7-y5LJ&)wp^BJSq+}wf}$;>y#NU>{zJm;;v?bS z>g?}0OLzbYFBIFP72OR)iytm%$%-rjvaV82Wg~}*8YA3>Ff_+=wnFy?HgE02gN@#UAKSNIdVPGj zyIA#+^Y8TGK=b@pu>+~suWOz6E(Vb29vQ-yiXAH63 z#_GR$*WR4dbQ?(*Gs0UYcJth^7flLbhz8?T;PM z54`%lb484Sq5r4b(A9HIf&AAenw{_}4q?u34DbdVeeP@oS({Vnne@G=%SR|~ITzXF zfWx!VmAO1FD2F1uCcUQvdbs+5Npd!-Q`E%49av{+xA#QqL@~rX3xg~j4Pv1xx_tn4 zYJUustBX&XSF+NQlTgQjJR`wQ{`+I7oOxgjB@x;UP7rG(7Fh7D9ss8aIW8%oeAWLL zLzNp^`<6+D+HO`FLF+2Jp_#Vki=1JT4V5)U4y*C7dRKHAj0$-W35Lr%RC1R4TUIm} zYAaUleyP>RFpcUMR)mGU(Q`ID4a<*%=3iv+BlH`r!r853e?2mqYS^M}H z#2~#v^Y~{X|Eta${%F2|a{qtoydhkWpI=~Rc3RMC|N1gydUpN&v?aJ>DG2T<@b>-J z^|zK=*9IInR(K^Y7?I(>O%;!6)j1xKLSllz20aBmz6fEXVw~(My&;v2~Tmz>@`ag-8Q*EujHQKEfv~aCEWz)*-?^waC+L-*FVQsi?&8ZSf=Ev%Hq^h_09i#P9VP-cAxc8G6bVseNW0v#CZQ54dHM22UP75@MG%=0V@# z6L&XBNM(Jlc1l6g_>ARwfx05(GK&|*hp+YIs@3`Q;7!4RB47IYREnRVbCJ8dRW6rH z@z9;AqcJnm!^pmiQ`!S;3iBbrt{}eVy~?ditZxkkLq7~5YZ`vgSxo1QN2Wr!Q)Q=j z)m}E?Y2Hf|N64imVv>CX$h&V0@~MFKR*n6;uF zG6TB2CE_c@#B{(yFF{meBTp8+-VagU0?I8(3{RYuA#`qt$m3)~!oi29_xEb~=7F_Z z>$py^O?(3}Rl#@#2|iznpO*o`E^SadF!jxrvX(6-=uOs@X+dY&I-xGdMxiM=Q_^(X?#7L+0EOBIJDvm2#Kh!a&~(heY3=@$$8!kFe90JXgplPGxf+Af1QEj&39 z|FSK;LF`zXuYKpbg*mJKUbgb_$gnwo@n>ohlxsg|^krD)#Z)7>S^cbOxi_;ZTs`%c zMYU&(QOR7BEpNTinswJWhQ*{w$!~BZL`m9?8dTj0JF$4k{)o%hc*9i{g2mBidIt~| zt*&_JxnV`qHht}P$vodpzm&&B*caVs^Io(Q@9JrY?nMb)4Q7oQEC2r7Nh9kWA4{Fn z4rQsN<-ID;@@mlYw4pO^e_ON@$ot0h>8ii4!qUPIwNt7*?8asK%r~swo}Fl)KIHu+ zZOIpw6w^Zee&>bnnR}ba=IOGOO<$iD$sBA?7@yrZapN? z)e;6Wr@XcL)<(!R1HAMe%^#Xs2DkO~txMCs&TN*ZXcy*Y5$r`*0UWhPEE=a+@SKg} zgQUKRB;!T|^-z|$TPTEjGdac}XPK0w&y)P-w6U0(6)Z%@MhAWz_+MaL%%~-3R=!v> z$SHZpIkl*pA>Gh6poz{hhQS=67lbIZq$wgJJb&L-=NaPYqQ8%j&Ag?E zOu^+cFpc}LoL5BB3Kr}eL4#+>J8I_S1-8N~(}2C<3KI={h3L5pj^c-lJ12FDYYm8H zoL)vr6}6@~SrKTyCtAudk^MUiQYV@u&EYR(@{Ik!5pp;}_lnRz0O$&Mp%Ir@@(n2xu>la-r+s@1M<+}@_YDqnEV zFnPe%eyEjub^7&d(pj0y3Vm0{+Je0f4=^3R*!WuQ@RN>}j*%qV{bT1(KYGNurq^=W zv8CC0XG4z$bkMZZe(Ls8ysX7<)4wISEUy&ppv&@QHhm}s(<1vev{U|(x z&G(&$ND?n3@G=3fhg3zwxNVcC0=?bqW==9i#=>r32oE$pB0fPoRVHjPTTXC0i{&z*(d2M}pmcch({c zfU{N|>;@+T&brV-8gSNG$_0S4CjY}(SD0GYm5Jjo$W;MzEzlZkMdcsOrJ7 zj*5fqyA?f=>`AuLSOl`4TxnZZX{a?2Oz#7S~BZ2=1dg@AkK|Fr_ z{fGC0{C>ahNM{Os-1u!zP@}t}kIAz5Iui5k3Wf~UJQM#-GDm_fu`4DI1!E^93-z!G zO~H^cz}r@i5;Q7*aJ@4zJh`$QYi)?J?^gzMwg(pbnf3tz+U)Ywgt%BvJ~1Mm&*-^( zb}>oQT4b_pX{?+$t5~ewQOUz*O*t^%_2M8;u_MQ|q^{^L4awfdK6SnW z0eaq{Lb=Q$uXzq^;ToM)`OXi0aH5~^dgZ6MJpD#rF#YX;i0`5omM^4DCSAOT9u8CH zYVH3y>?7Q+e)kalv(f1y0%a$>yP7=AdR{tvYANdT(@uL&UhrmtjKRs0YuV4guH4fN zHcPmSU>JhC4_&iGAyX-??!LFer`+XkyQ$O^*t%q!aqNMwn-{}uPIv|CbL>yT-e_;x zbR-9Jmv(5L#r>4iIWww}&KclY<*F%w2;l~x>^U;xZGD$-0 zisw^{IoJB>EX~355}qLK7Ta>~{-b8YV@oz%bYW7T}gCy5b-#>>G>M6`lb+z>EhX4AXx_7yb;e06E_fKu`UT@~__L zWL8<1#r>Vh+F!96uNm* zbSb#;7FW3r;R)QX{E$vIi21-IJ4}n99WC&oIZk2(6Mpp5p?HllKNGSqzYIqm#(%y) z#mhW2T&1jJD4mU~KX6;lv~w?8<*2H6QGlCZ)`L(Nv_>toAmS?KXk`Qro>X-|8*yY=gZB}cCPF}@Ieb79F4fr zp6js7^e+2S9!Kx@;9`F0Mc#l4@61HRfc$BJ#js>nhE7}k+xDvB(7~Q)TD7Z0OqbXA zRo0-)VV=b3*R9{TUw=x!r~Wo9g7^N79#x_$^-kaPy2hLAx1%9ucdtE7h=xtDuvclN zsXS3^>0%mtlXHa^yjmcGJ04ZJS>7ER}ZNBSFD?;(QR_d~T2!)}3)J0S)LhD(>W`%pIw19WvmT$-Y<;eb#r zir>Z0;WT3<0ThgboR1{)Yf>GtUu=4M!!OFn3bD%sIg4=eL5knS=y)o*u-^bo0Otaz z=_xNqpd?1xL|TucVysGYFk%cFfZckBg}7nZKue~uAbGHv5ilJu!yxe?!*@s$-&heb z#~jI7zYo$*z6SsHvL`OxRxg(j(+*H|9rQpZo!-1SiBRAVU8Ne04w%r6ppzPFT z5G^lowjVfHMM!1bD<&HkI5}hw_98A$|Cy})8w;fTvyAN*w*7w(3H&EzfCeD}ydZvV z{+)o(#r<2U3c~!x<_6&c-}(T1*1JzXJ_-7O@l$in(q^$&f3l|)^||5?lfpIdmIj*< z49Z}fYEyF=wCC0j^}3PP5e(!AzgRfzt7dT)XwAr0isV)@9PHy9Iw;OCHN2P4^$KQ9 z9UC!JWw`#^51KE_Na5As`(_oXqO>wnNo|4c3s2q@(yiK+$rmPiCMh?~e(oPIhB;(ot!uwHX;0EM|*aJcPw?=1cGWx-fGF$OjU zeQm$Fs|O10^EMKqyXh>9kF+w>+q=AN-S%o+?(PI%d!;J)%xfql$6HavVJ`{C(em+a zA;I(<&VTA7g%%C^so6-rYAzw3<>EoNqEc)m#&Hqf9e1cQ4ED*EzQz>=dlVvTnJ|0wJOzFY zzY?Pdx8j7KzyzDs^{CA5LOa!bRY8TujQs0e4M`Hfe{-9l(MT7#WF!BRc4M-DLv84b%)L2-CvcMkv!4Dv54o+)Nqf#qX zujim4t4&aC+K;xL(iVv`J{B6#v5C#wj(oco7~?*=DF5^pae{FwJ<56-Q6vzPDk`eq+KM*34f zj^S?`v|rx?`h>dQlfl1O;uo2O|9t$=CWI#ly_tSPzzbajZYeAH->$Ar^Lz867wK67 zQ|mi#2?Abxp9a`dw8p#iNs%-qFn$*x3Q`OOE*$M!=1MqBx^j$oQ(`kB3&bovIoee( z@<5588>&TGiWo?r)+l8}oh&JV1PF9Ss`(|D?41P7c~1?L)>;DQHc8D5U| z-UA0moqFvOgji zXMG1xG>+RBR2^hD&W=iENAjLL#EEbi05#Z3Fg>qXbES}d8EHfCLJXej`OsFQjsUAq zBwSR=hqMD-gv5O(lLME{C~c13DEIFFgC9iFz2!On&Mu<%_blPB>A`=G`TRaRydaE^ zzrYKH0q540{4XaK7AFb(-q@L$0>SyEPYW*uJ$cdZ=_vxQw)Pf@!3JHL=j0jVe}L@_ z4x50Gh!C0A(ETogJt25H?p(HsTHSID}kXF3HZ&m#eiIYC)9uX`#qZ zjfW$(a34h^Fiurg)wwDF0f!B&nFzHV8%(q(YOSKZz8aJ@zEEO|ou!xY!7a~)=}~bj|ieUG$45#re@Y0) zC-QPsP@60^jMe=VIM3A%=LFJ5n)PUGvB^pl=-UV|!WV3TQ4)-2#?up>b8zF3<3a__ zC!CDXqG#OZmuzzk$())37Tb+!M0ZP+m_?l;m`SK6dI4PUv4CyA+E(vn-##K~Gjfn!?sXZtYUDj9YOY83~= zHjq9vo631h!uYP}md_CB>sW9TFn}06??@@n+iiun?xzp;Y&59O)uM41{G<*~6=v%; z)EbZ~Ic1GdHSM;z4FdF)XL1dx_tOJ77SSc#*EW5??;{qcw(29GC%C>;@x@C+Y`xx;|shv z6br-k0TCoS!I#=~N$4qzOip?we}rGa^T$iDiLO0$HPF3vWHu(z6(YOqej_n9MYc#+ zqcNpK92Q{Es~9InJieEmcA$Ht?RJ?yH4S^;=CtBsV;)nIdgnx`hlP?^$rrOqz1$bM zn^ey?_ZQbrDJeCZ9~~R69`p0COw6roVK%!5n|M-k1#!{LKcFpsbCt_=TxpSf0HNKw z(c9(KP{|+gYV#r6#ZjZ0wH|-tV*zRbo6HKV%Eg7vNrxo+h|7noRC-5zc`E3K>XkZY zsUhB;2ipW64QvG5Eg0!wxIasp3eHcmNjd0NRM56{jO(tYX<|qMV&E0OOzEigH&+(E!*G=`T>CJ`K5@8@VTEgg%()@!`oefUG7<|@7E4BZ zNW70Nv|{=Z8=0b<%m~*`yeeCQ`xYhU#(qUNDFzE}JC(%E5i7|dZL+T05g%MKOUqUk z-4MsvezP#RB7FhI#>-|MLxQEt6Heb`aZjGne%DSRXKs%(!mB3?$P}(&l&(pPH;yWC zbpQAj)bvS+0_kz^qum{l#;;*}fUcy$CNh*2_6+Dsjw0=dHJqHWt8ur^#Cid#Mc4z^ z9_SO>&SlQB$#*sP&q%Mu!%930N?>E7Ls4K?`J@`o*P;H^kL~evI|wvDQxbAdXuSt2 zp@m<^!P9S7cAryCBA2+LRKb0YmPvPUyc$OTfjCr`+Wr5=`2{e%01U@pO$m@+0wavQ zUqB%L+PmgcdNvI3FUKyoSKi5Y!2mnF2a zwI$?^{rE9ic>Cv;u&c68zxW|D2AAL1U7;f#%nadUBCyWVIw5(KD}+eyud>Z$obp0`VnFu~2%;#p8VD zWokSQ&q2x(gKOnfIF|1#oUZ>u$tqnlR|I#)i(cuiHdj*X@l=A2`k17+*~h z$dJeLWy>nYrKU@q3_doW?P=9g;f`kynx70gKEYaY zQYDgo$ZKtUz@7f+O_Kh>mp^Mp=xy!7L*|c;upQQGy|QX0PyzE@opuDl3)@!< zG$Eh^+N>PVPAdm4zNB=#hUN3-eC6O|0xj~RG;!|T~-z{;U14 z(}ZhE(Fopp13K3H{b>&FLGCEpg)6!<@7M`xn{Vxbu7!M6?$S==$r%<=1Vicy(6x-o z?7~bQy-`HOX#!UQxAc14rXsA7WQQa&uD;L|#he)#Rp5d|Bt*(kMc7ZRK3~LO!D%;x zy#|}z*vEhyKCIp3!UB#1yNZ=IG1jMGe}Ke9xmlmPTS8*wHN|wIER*NVkK}EVBVoEI zDNN`Juuy!Ow#>ncZ2$T+pR~+_Mx3!5b5QZc;a86AlZY*!A(h6}Yu7z2hu`Mv1NQ;1 zy0~kx3rs>Ed)KcXLHQoh1%1W)25Pq2S<@;d@7iu&u!7dT?txA`8r7|CsrI_aH4X%z z?KU^1z^!|sj+9Rci9E2gCR6i=3c0h7Ihtmx~E;$=LlR9luvx7 z$QQ$6>N`pf?|Tl_rRuGx8awM-&xi~%+jO0i=)dts2naa-Vv%1!AOH3%|7Ap?11vBJ zcJy?1;{*0z7F8(F-#a=gC~y&%`y}sCA%EA{%uE-*7fbVGNm&-KtT^C~nOI&)`S4}k z6}z<+|8{4uBl^w+%27~V6NE83*wsN*X1r1;X}nut3_`Mo_!SBJ3F#_uhfuQS>Ye3o zFl$YwjE&6q$xEIJ)dJ~f45f5SbEbM!(6WIM$g_=YSTYNV>I#v@t;UvSvFcw;QD{u{ z$S-ua)w}9ZUwNw9wdumo(`XapD$}80+}h}5Cde(blwUaQ{lkWh>MxnRy|qJ1p?4EE zc`Xw(@*EzbetXbcH6?qA>t0ut*ay<1GeRd=^=(XM7&98Zzw9nfx%3oqbjqPro9;ZX zeB&;A^!XC!IO7}Nx|?5=KV0{bU;p84Fm!RoOO;uC+HIa=xbH~zNL@}ye!?m0UYe<1 zgwt9y`w6vcCs}+pda9D{#YC%z+TVQ@Nuon{)jGLmuHoh)kWF1Bd{3(=6{;pTXv2Jl6go+kFUo1!Fc z){K4&$R&9~iDs0sCXR<~%AEvDkOy;}L@J`rS$g4V3mV>9a;w@nhuMT|@KxDD9>lI1 z8<27oSn9D$(BO(#?&V%^gM1^g!XgKt3Wp~3gyWYi)!AU(rfAwRq}|2|u0Qfn_t(!s zuR=G8b$0{nB@e%hUbBVQ)f&)zIL(d7)%VZg>Il#gPdOR0`=d+<$dG8#VXpY_w93bh z8V*9R=EVAo8hJv}(NIQN&*)FfW%+Ti}T;yvH;fOFUI)e z9N|}NPy7q$0wjnh5b*5}AJOoF0l&Xpj=y90>9dFY7gPHOS~G3H1Z&eA+rQkL>~^~)Q)8|K~Sr+ASah?cNswASNm;TlN0=VPjIu`GMtxgFA=EY3C6;#$7K_?+y!MUwN0^-8?22BFeBH8hy^< z!CA#ucG^3#xDn|IT+27>6Ryu!R25pLc!s>jUptpk`qULKL|&@M4-Sm93k#uQitYM$wF))yR%1ZTBul$gc%kTFE+zD!Wc#NP0$4h!E8nU6b`1` z3!Ep4h-0ITx68+GS^LgplCCo+^GBM91fNg({k;LstJ^S>=Wn*7KrEb$v1r)?@Q!4? zYRjAx(8d0j5Ry=R2`NAx%zi|wmF%j?*Va06{oY2Lv5EM4kQfy=!yP#>g9Q9 z1mvA4U5=|*$hW$`ZV{>3WW+7hQ7e5LSjZ|h@3$OwwUn*L@dgfV(RH|dwnq%aYqm#? zRo-upk@RJnrT@+t_1`-YfLDIq^EXn9p@5(uz8T-z+$3=6_t#cdHnz0(_VwRyzX+q% zGS#=->wo^D3Gb8o^jT*zVeG~1tRTS?m-{-8p2FYr?&BPu-;L$*N=aG#%*KzGE|}#H zvnQV4#kb30VF#F<>k3rmc!ea|%iQw2ZU}b!W@MOZ<=((o#W8{?MU9vIv?_I!dy5rF zzS6bbswtEc4i_hfr_@f~9Nx>$L$yvAVqY7?5oI_WwrZA*{GC%mKb7m1jzx3#!L0Ew zl1YlEZ=}smHnW{luWt(YbsOF* z|BJZy{%g8lx4aVwJ!l#v^n~7wuGEwpPr31jKE=? z0f~YSKV#JiNm0Mmr{sZ}q8A2Rg9j|lpt&Gm zX;?JgWtOwkaduBqJ<#tHlZJTW^fc#t{A=+rSHRLR;`}0%&;wJIiJYv(-ea}qBYV$5 z&J(3faWf#{Vd^=++|(`CbBtr21Jm1Hs{#?F`*ayjVD{9KxWjUp%>q zm$Yg^YGF)l5z; zFV9@goSPqy<9o8Q^OT3r6H`dJyO8#1_pq?6*nU260K^mZ8@G)!$@ zNKJaUViW7(6t6}+wxQvW!fNtf`1ssQ)EhJ zQY|a#{eJOuMX^QWuY~A@qqD8-sPFLkBJ^__&%5CtrUTblKZ$rIp$bliJn?Y$OeGL6 zBt1S>jcB}(OLLPmnz{&y%@$l|p~~!hO2sXR%Oy>r+>L`Jv(sP3$T7A#vv7iP`SfJC z!4@ek6I&!&S!S(iq{zEZd@Ks=Bz)j)HA7dp#^-8&H2l}#Zzbb78>g-M>UpM-U+R*{ zZRml`B_00f#pfLS(AK~N*r>utubK^Y$AG@sDVu=7xtha#&Jv3yzs{H2F{Q>0+%XWy zhv+Q!&XS^^Smh5~g>RM+HGimWs?o%>-qS%oG9nWTak5(^>Wqt7glgR%T1Ghi;@Z+$ zz=`l^!}rG^Mpi(L993Yx{R%@buANqCXxW_t!RX37wK^(r!q=f>fSL%{^KxSngwa%~ z&N!sfAJp|;obVR)dRrO9x&-|uz&<1h>FYIt;NetgpXsaWt36JAEq4>UXa<1E4gviT zsxiTNa_aJEosINmy?&?q8Ag0l^9OKr(8Qet$e6cL1?2k&=V)+(EWVkQ_GW+Mq$hUD zp6YyHni$m!DrUb7SiyPqr*s$Ug4ltrXLW4+`qH1+Sq8+1Xj^?HJ};vt&&z(p62ZcMF@k^*@jt}1{|bfkFGP?&HHot5sG z!tJ{+ z;*=Z9z|uj~Qm9pSKO#VpBff_##RYMU6XU+cD+T3w2wl)I?U|x`l+jbrsp7k^`d9eO z$_^>8Ebl#2{A6t9DGf}4cibi$;(W_GQ_I8-Rbz5yKHSE1!m9mNPddZ+=T#LM@M?b+ z$B?;QX3r2OsZM^vA>TE;|E$CbIwd^4 ztG28#?lL>ie}`=%pXp7qx5E97fbRADaNA&$S1(eNz+?RtFriL$YQILaCUKtXXzI6v zqeZ&IIf7B9PohJvdc;IO4&J{NpwDKoni&0Y_xnKNhx}V2M|B$C_|Sc*e-qeN84`AeS7eP|~J)Yk8Cw5rzsaJ+?=lDb2EOBA*^T%cJ5)n~D$%b>eb^yBeydYs$#*xt?|g@?kwHXGpT9(tbJY>6%S}DLbO&N5@AO-9! zGXn*RX|;pYoZhI2T3z8vSL&s{rL^-dSh9z5yt2$2O$>}azsw0k_EP?x+S*G25-0Rw zvH%r3o#N#uRD=5d;?j`V;W_qpK}e2LrwIfGK*mui`P%g)qjzI$g5WP?(X z5*;HS5bpN{mF%552z6qruWZL?+$U?2ud@1S8#ZFfs-Gp(*S~QBB}1F*NZofW&nL3e z7vvB|Z3}=TceJs7h+0V*`xYb2Gf+2q894b^>z+GLH+99-&mY4+1aBp@7YFXt7M812m-g#|Kef4h3 zRc|nbemCNcbnd^)HNRo-1cA(Iw2_VoPS#oq?st+3L8c^VRJLqL-QmB(Z`RfFLL%QO z-)rPkVD^;I(}~j)0f<5%gn@ zB6b?3XY_&pceJfv>dOTALQ7(NQ&;Rmm++FQff-iId1_ucS4Du-X6Pv270fs zHDhjWCze>NE5Hd(R@2| zV_|{Hj2B#K8p4fpah9`(;{r&6PC(*|b#g$nER4>{=vk^OAvpAo`ZR16K>QXAs$;}E zd@QZ1s^eyxgVg&CzgZ(4?MkhO3bIjl#uf{y$0^JB;DMVusu!Em*rUPu&I3S6W&d6} zgi*8aL|Ia_Kfe8wxZxYru(k?_g#y{W^Ng+i9Hb8b4gmm;C0xidu+&MZq1v#E=F#uI z3aib#*)8^ygv$(7HoKyFy~SO;M$&#^Hi?iZrS) zi~CT_^>4s=o!dInd;m&?8FuI55c~Oo94GiwYuPyTr4Kn+TmflQBq{M9SrY@uoq)eM z5B`UK?4Q*6ryHz)B5zE2aT!^>IhlF9H~%;_lBUa*k(pCfT~oo!6$rUogoKuAbJyPQ ztl{=Uw-iB3+EsYE28VdAP$xZ1hW9IH56#@q^hS?Q6!pbVKU=MS?g5lTn9{J7FJIT- zux^E$Tg%r6_JHeY&Y0$dmhGGTGcPzWk*c}03*Wv{xqA$dV`|jH^2i31{&W0TZ$-CM zCYl7+oiJEun&U+Y4qk#OhE8=fhj--)rMa~TqQLKjc!K~b>m3IiFYL~~+7jxry8M9o z7RFZxM62G`FxnMVg|0So!D*T_d5>o3o4Nlq1tnROB8S5tSPjiB=5+S}t^2uYbJ?rY z;UzZrPcBQBe|YHg$<5c{kuzlPXXL&2FQiOamYHzduP6*D-KyF3$scmZb9?=wwP+6= zB<^V3{-{{FzVOV_G)M07{6Y3?YekFTvrb=kv98ml;GcIG->r3B&(W&x=tOOve2(dS z!^wCU;Uy<+Vbd}8>87W`;YHit(cqgNvavK*M!tM~?ygYp@siFf^XT=MXRg5`gDIh5 z${7YW_Sv~2zGfL*@fla2V8@#vNzcRpyEjMJvj&>NdH{a>jwwbT`Qd9fP)`P4#E8i7H!AJs(a>+qt^$8H-zPDl`&{eOf{dO9Aer<)ZTOLSt?e7GMDD>FSBY% zuN!O$QBw}w6tuMr!2tMKb2JrYpU#SQbn1(^Yz3q_o={w0skfwY+C~-|N@mMaOIVFJ zM=CT#0Cj-H~na&?5<_O<&`>c0i63Q!^ApX1kH_)Vd190#!$>qp)U%@eWw^i4c0L2slUTl+WLQQJ~>bRcC1(3!xaF;_WqQBrwn=u{LM}d7=bgQ$wgnd4}sa$cWVlzKvEg61FZRFxd zU>r5YUSw%JY!or#oICU%!{Gr6f~K z%6Ir@ zs1f@ai8V6fF@4hVurzugZcy5Hh313$XvVQ~%jK@S%Q7b#yv=9NI^{j|;Ig6EhOT(X z5=}guUVK}+ESn)zeb@Lq4|}9tu*Rz^Qb`e`{Y9{*7(ntgTGohP4USgv85+&g_3#i> zdA;>UQT%m99f+j3j9g3a3J(<4%39u zlx{#GM+rbPhWVY`Nd)Y{N74=-6dD_LiO%5{6b8sW4OcLu2sN>JchUGWYS z6|=Yu7l!`LS6HdxmwW`R9!w}2eCOhC((Iqk05)ySv$N6hb36q~D>!jE2y{M=t@$EE zslx=~2S^-Ur*sk1++-)E`VPx3nw}~^>-AL-+NuIjD*jfEq^#6O<{23?zb9wWBff*}4 zULuYd!y8THivay`RxQbx8VUag%Fh`wo{YgfhpJ zB=BVC15UuX;mzBPK%Ngj^LQMk_xtC^?FvsG{FtO(r~oTOj%H7alr*oGgxUx~i5MgO z7$WOV#kg_=->?GCBz~$D7pWC{)wFiDpCX{da0Ng1Mo2;jbWAV5$Y$CW=sr|T39o>g z%azMWfH7F+m}v87nOz;4g-c9*d3CZ~5Wmq+X5dqfNdY3uOgp*lPJ@z;VqTGC$Wul9G!wvXHWP+v-POWlc2 z?2rA`y%jVf!!a(}vA)F1Hjv*o+h{3x%4Wvo-1~<#>=UmU$Ys!@;BCs)rSYv2ndWm{ z8M`x+o_sokVM{l^iQkp)se)WsUzG>L@C(B&ddH(d)JNe;S4jCutJRwB1RiSth?%>4)gEB&HkK z;Xtw0z$>COTP4S`bSs6fB`R0mX+ewV6A>F;y0QmO*U|+2Z6P& zaHub^+Rsg3N(6Z;A2JH(*-}VnJ^97M1kxr8H?yLbONFSdIStTI#Riy)pe9#|eF%C9 zG?j`y#<5oG_uR@!Nvjx-`~i)5i>C9@X_=w|YP=1nA&TIX%ZI7cVj8e^U~naf^HV}? z6?E)jSidZd=h|R?Fj7$)QmoidR)1%#wSkU5d1Pig_Kf|NHJ?(m^_qxMkEOJkY4K%@ zAwTsu<*dX`o2i=g!Yf#52P%m`uvoyeQ2+Q#brT2V3|Po{+DEt9IHCn|6$7>CRc3(U zcx$}I92i<3k=EsAxQog?JfB4Q0V*-lIp7p zAtD@$;|WzoBH^4dW`x@jY3Y$f+;wI|91@&l$dyrCQp^>Aq0dLc3iXOg8=G+DzKnU* zaRqf64b45Jt=^D(9Y|=GdTQ@zdbl_GaR;=si!WfLKZY-iD$)8_{cDohD~+YVxv!N@k}X)zvNNGNPo$;@-c&t-}Y>t00>g_ ziM{j{Hb__z_NWotU;#~fR$xsfom=~EqOp#n%zkm~8iwkWV~cx*)tot4*5R2lO6*>P zM6vT`?c@WpWNn+%;e*4O`K!D=_K^2mm%r?z2cvpZPeXHcOhz`ZhfXhrD7#7I$T&Rv zQPH*|0a4?Cy09OxJm((_^Vxf#?YS_f86~32HhyKLHz+Fa&C$oqmt-dfI+vx8%+2o; zrtNz_7=mtjK!X}rH_s`aVy|4jB{ulITb48S%y#2|;#pa?h5Wh0m7n)yI5(dfm_BC+ z_?{*{OrrhvHYe6t_Xph?pOYu~PDJ%=HZhWkv6>VDn;9O691BxAc8vZcoyH#1$SLPY zg*66BB>WGYOIW^E&>P;?G~8VUX582^HKuw6)r2O$wAExqL2$X4jo&^tP3tQ?Ijz{I zq|ZpVmI%qr@F}skq4%7(goq7Y2Mpk15|_8a}qHvt~Rjd$EZ|8I~g|*rt&yqft!FRF{zfaK%|W^cK1S50&5Up z1s zY{4oyzDI$p0q147FP8hr!IJPs+SB)-S%MylRa3}~FcfDyIkBgQo~(`9N8NjUO^p4+ zb-un<>mQ;K4}nVts8g3Q&BD~w9-pAyR-0BccV}MZ;Gfv;zP7q=^QGtHl&2zI0J4*8 zwC^+(T5_nMTLO|o z9=#;cNq$)i_;;fDZ$nK>`n&{^`}g|5Gk$Tvx>z+Vi}QIt=CIx~7ieAX@U= zN=QsfzQq&!n^3VPT(?v16x@yqU`Avh!8ry6)isHJ82U0KtWuXhthOTs%YTKjv?-&a zRh{epKuwV+r1v2b+NZ{MZ*V3N%jbz6e+VV_O+R~?fOV&CTx^2(39Rj<2)LlPwjZus z1OBj-VSC7jZKhc6eFoc23R6?~uiZX2`R0;`;+RaF?PmjhhO5kr5q?z_Ni%jWW`sSN2!7mn{RD=B97N9?FFTqZmi00v>x;Ve&6 z4XusQ*;POD3%#^H37F7YZsoo5Hep(59*1S(y5)2Tg`T%qFppEO43(IzORBI(*S)y5 z-VP)~M_Ja=;m6@jJtRMhR#>;Nae>TCc3%qaW{K_0_~LNPeLulh0_2_kl9pL63E2DR z6G&s5zQ>`(^wP~n?lpxnx?`Pbb^P0Ui3jN6;wBi6!gjm;qEWPzg&XG?pIw7;0}V5- ziQyg7j+dug*1n+*-i&#EX$tBz@IVm=E|hyx+hjw=9&GR2AXTph9yvWP8#cTR2+FU?oQ*Z`$^Me;n z#$|WIfjmzZ8TTu0=WzJM%ht}sEPo~Pq^Xv*8Y!n)!cYhiUg{^WGETvYLeB9Qm=Z-Me&CNUYxh1NM5(L!i6biq0YY689igGh z9Saggy@=!o(`wp6UaH$+2-9`CfMlX^SJx^gAbb&Y9xQbA=clP&>cblqA=2=v>ogGu z_1dsQKrzVzRUc$qt9c!U72gdlw6T1{L`97+IdPRU>45O?;clo^YlfouU>TL#Yqf{l zn$itcGIY8$a<|ROqQSYc#m$b3z^0~N{0k0RDv03zDfGaQx-V8Mn!;n`i~E%{uS@F|Boo8P<#rps8({`{?hA_WA_`OQA8q+po-#% zHRp*WrKaI{2){vO(+cok$7>9L7Kk0)$?%QW)&YW?V4EkOw3vkuDMF%t$tLE?uA6ix< znW|OOXngOqQhl>a(|~jLAv!zgjKQl08Qtw!^VmzMs0Z(TgvXeYr!P>tkYm%A(!j66 zLOzGNL3e>eW9Prfuy-EaV|wwUQ-(7?#`pbdel9eyCL=Vm_u<`fE5FpD@NB<9;_c73 zO8N*8YuPLWy?NQ1KXW?o3LMV+krc}Ej_%kgx;v=FG3HpX)aQ8qLwA zLQOOy#R*R^mJ^AEJ~&V^PNF##ruZ3C9EK10xCZm zeqK6R>IZL)Fq~;dx468Vp5hx%147E(2rzaC=39$zkaW#9(^M2=8Q*gd@fNSEg}w(q zI9Gq-Y3Heqy=xV zj4q-ZkWhp9xMBf&$lJJKXH!rhhT{ukG#`x~Vsd4^SLNu1;ncy))!@U3e^XC?PpKzr zf33s&heR6)nf~eHdmysFn9@ouSmqz^TWtNE?3NRN+5J;`D~UW)YD zNGMl=J+3x1tVy4g$~h=DT7*SxYaE}(_;ar)kfIFY5i=_&MgyTN5G{j4V0 zb-Wgy7eHgp4(ih~Rbt9(@aCmMEPfNf0?Fo-$W>PJ-pu1${(| zWp;#uI*HckmX-t~$<_`CqKC;IMgs;o)4JA@nbYzk1b#+C}M3vhIaJm+Xjua!f^f=M`3Q%H%^WnUF z5cFZS##KhS2`^Bz>T97zjTV?GF+C{#e&&NC&0@lBy6gVr9+U(@lI2UJ3ITC^%c(&d z2K|8oK-D~OasB58iDuroY=@PNCyETxGb|_=WTXz8XtMFUnKJNMz+&jpo@Q|a>yy$T zKhz?MsR=_HGD>(X5>+O3ec;JyNmhFCT+oyqe*&o6Of!M?p0{GEAmlRus@Rv#X>Eot z+5u3F=i@dZww5H#-%2W?IsTSm5MRDy=;R9xJn?8%_wayDn&{Jiw5Ct?AC`)V0`XIR zE2)iC4au*YnHIiZw{lJX;$*y*MYNvj#D8o80NU!GSU&o%5^F&uO}#u=@kCC}xxJp% z5Ygv50I~Q4gl@)oM8+p1#wObf@!d|$Oh}JT2GC{78<16;>Bk%MTV)j(Wt21|@&W9s zM1pJe1REce2zq1a+mW#QI`!SdQJ#!#eU%*pYQjS!vw?iVZjgydBy>s@`|SA!AJzrE zG6{V;fZKSVh;yPQzv_ce`R;#@6mUR)dUM)kXN2of6k*dJhn-rb(#OKI3taTaTcb}f zY^TZluiq2LbU1B(-cRjJe(z}K1@m)#a4l57RM*QbQZq;D<7R$b>pbe1QNh2FGMX_K zO|N8EnWmMwjXI&Fp{=k{^Ges)gbE+K)XdLTr+PVnW%r6Mf3vWLl zPT~y2d3~&+vQ-;Ye*F#$sO`*~Eb>ZasEF+$aET)}in0|Dc`$L$9IGK;VVQ7o9Qo7f zMo->H*52ZjPlLw6A6><3jC>iU&VASu<#G-lXsc+iSvI-2cTxDk{r68yqJs|uPDy_- z8-q8lmM)&o+C}uuszX;8t`nWY40fMDJ{@U4>u%rV-co!dLmQT1upVB7VcvtE{A_=0 zhdHrqHLrU6*R3?Lbn_Dd6CNk}K+TIgOAy$Tvjt5AyK_yDpIB7rH z!l990qC*89ekzIY_9<~nV8-D%6SGjkfYO>;fa$7jM!N|2*uhIuFlL$qsU@yBP`ac< zu~9tpMGOtWP~B=ENtd>C!-U^`k>z~WwJv&hW6$QSTqOywO5UJRw1XF1@uOgY>4YUa zAhJ@6>{yI%+|APSn<}uYhT^z&Bf~Xz5tcJRSOun~C0kvsJxu~#n8}NWJJHfAfr5u* zNcj&Xri2hNOIv+~mP&nPVReEydk9bNS5E{>5IfsNy;zNiJTrx6cSnZVG1UsANnq%v z@J)4)nnRr>qE&Z_6+^`govwE4mXE+!E8SwJ>5|kUyavaMM(+{94`~O1nO*8!J(aqj zsersSC+xi@Q?3rp11dN{Rh8Mio#vi`64tL*2{3>N+UHex8!BK9VAAq0dH393ww&tKb7aG}^thq~VUm zI#HYcz9pnIKK&!(YYJovnp%eZgS*#{3s z-z6t;Gqi+noZ(3iDS!2n_$XQARAz`?8_gelW^^jqFdind{N`b-N(=-PfJs z7|92JO@Cm~toGO~>7M(p?=jDzpbY`q0S3rzT0Av8de)dr?&2Z8?9qx`>&+W#x~ zN~oHy8MxT>@{RGgpvL72zOEmW?~gw51xH3Hi(L+njk_%*Moe}QjZe=A6HUpn5xtpN z5HFCOg5t_G!4_5(V2J?4xXTT%RJXF-sk#=&12r&J7fQ0pGX$bSHJHDOoti%nR%1+8rEaI99Im z2fk?33ceaJ(i|5kyd^A*2A0+ebMb<%Syk;$h;U~oXF3X|n`9Q3LI~I_r%@TzCjI)E zEQ*4R`QUZn-I>y2g!AmhhV{=^6d;b!QMJWbZBWJa8kR4u!IF#Q+3M>IHkLLQ#K5fB zi_yUe`9oaPc4>-vS?l77GUsnX^8$CCTBw%I8s87x5-TyI=DQ_;-y3bJ8L@AuXlq;v zxz0DcvV4D1v|r$Q7aQhU{pK&v%Q8tfwS9K0%G=~m^o(A6Q_zk+dU1>4%MB-MUdf_O z#d9*WotOM?OHR;!ilsYK@`yPxW_k1NaM39TS5U71u(S*|!Y;%Q8~LChYap2E*YqIm zN#*Ks3k^fdV;rZg556#ZSV4mr!_(MD6V5Lfq8i`s8R3+G;qqroEP#0o0{DuB>u#-u zFtVQbrjSllDef!|F`{?m*JncU^9x!0IO5L~(idUK&<~Mb&$71)EXxkx#SGHgMs%5` zyMF6)LxO0d@nH|u$U1jo4Q0x%LVZA>gsVwq=)1m$EF`*##jP;q4#UA3PvC~1 zw}thU)-59F!ra$*xWf`n1bxWEI7t4G#F`Ky?qNj)L*CuRn#*;95m-9@R1r0rHr@^c zBz?HF8-s=#1)mQ*Y6Hqll35Aq-Fd(TE;enJzE<4@K@VzR@JK^dvVQ1=qUe+QU_uqu zyXh>nhw(^isxM~m@Cac;hz3hIaS;5@xt-i;I+U~h$~UzhQkSpGw|--7EP8Qku%Vw~pdvUe8{Tx0o0n-I$p z-fW2|Q*0ZC%dnUv;?1J-{;+r z#~ENJ{~M|N{|~(U=4P!Mtasa&R%GK|?n{5GZ(N?=?F{FUXn9YFa1#>y-&gR z)T8@j@a3nULF2?{0ZK0~`h1kV#x&x$#zIARB#}##0+owY|spj$|&!qId8A9 zz<@~xVqKR;Q~*S*gB!)CJ;>r>X@)-~)tJqd>Ij1|#TSIDB*sMpcCuCW2UF}4h}96k zTC@#V%Jn$>v1Nr{SG_GlEPKdupK5sQi+3L9?3mj`6V`vY)b9QI3!{lA%%Io+uRsnmB>&qRil=C&slD>PVS?`j}E?GTM%nB5*mU(~On5#Hv zVW}+W{t?aB&jK5J5pl1+gry(u^L}M79F{wJ>#740WbgZ_;-iC{y(=CQ z&|4ggm9Zl>W22g?Ndr+Z-;zEe>LKrOHXL)J%;`GUXWkdES(4@OHM2Js&Qa>+)z0kV z+AWj>c!YwBsHJBrDA`M3b~!l!Z-e)TktoN{f<8Il`%BUQOPls%%-kLgDou{1D(tXyvR zMYE_*3wsp7Xjf^iN>l7SXMpU#BCVXEtpyN_BtQ@=$koCs=b0*!DQ0lRafojyZi=9% zx-CaTRsNaZ8@1(3I*fpcPIf9+8Dn7LMo@Z3HcNysen3N1(rgg?F~6PMWm?{S5-bzF zTZf$c3<38=!bxNr4e`Tb6-!IMD`&%fTV&+vp{WCxjv5I5tS2Fflg)r-gbivd($kW^ z|F(>|+JxrYA-vB|U}y=WpaF@;8Sjy-r-ED2^t!pAX*=ojV5&W3rGP7fdk6S@*ur;p zkf1z%&?rM-97y05>gdvumVMdI`$I;6_6ov-S*s2?nZuiBsxwTPh(h5+ z-cW}b<&}7icN!aFw5(sZ-m|@9;K~hNg|OWQ^+%`O&GG&As=BH%l>c8I&DYGE)y@Jc z%%b9%MfE(uvMaZW0hR?`%l;E*y7IbKaN z!K}(Wc+ywJtik_=epB0;=>71QG|R_?K+iBD$-pThF3u@BDOounK0P5N%U38PFI_0R z&?-2;BtbkI#eLVLv^G5j;22-EfmGeSIH_o6L?aU1sw>&iPmm;H=zEZ`N7`wRpGMwf z>>h1=IH6WNwe0HR3z>b6gf6Hqt*oqGMz24I&QI*@zACq+9(g+oUx@wqE74kVZQO#{ zzRK|zB3&e!1nLW8GMqNngy&>=oy4&RskXRW5XJPnq0lYq8Y!miP|n47T*Dc{WxBA@ z)(Sok>7A_z((C?I|FmH{i(*Qppm@tUoz= zFK>zEg+9P9f`CGY_+XpSY$HpIqn_b?#yIN>=k8laNV*>QMv1juy;p4m39bk7FN*86 zHi$Vi$qzN0VuyP;6WOYKI#I@B)z+=m!zsIBWRkOM%!_0><9_|Qwf4^ZFT6vZ-kmYK zm6gMH+j`$%`?c1dpCwiY4*kt)>SDufyT5)6vNJci9e8V>eHB?N_UcK}r1sT@bCZIm9$Bc07lHm8)o`+QN^J)?_e->2c0+FUHwyGK`c*QJX^UDryb)^=sgXIriPAc! z?{a-MbOLfWb`d9<7cHpn%SOC1VN0DVuUAz}rI^6w#)&?GxP5SeDOTEBuf)D18-nT4 z+Ae3I{b3ELFvDp}bA&K#E!mQD%~zc%Q)jRf0>=-i@>mCvfYHfXQ_n@fKf>unMhsb5 zZN;7dD$Ev8Vc6-Xp`a@VkzI;ANiay`E^Wi)`~g zE4baH2`N@o*>>ZE;C>)_EIOyrf7r-oklIHX_MThm@*DvLFOrwxIl)i%!D;H_Y5FqR zY&B_Q_b6Z*ixBfCD+d(C1&yE`31YGkS6%Sx^(Nj=FI&TW=GKU(HijL;h2AN^L+H7O zH!?3B46T_ zVXLP1X5WVWYApSJ>?#uyN+e0h5aQx>g-OXN_QDAn8Ih?uDKVLMg1mBzqNNJTT!e#* zt3!j+@1VHLO+0Jbh<+IW#Q2*HquL+YJH<02I+5U>#E0YwuljH{`XMB2R9kj(@rrCX zHLhPQ4t*=^v)jVAO=2pI&r`CxTZG;uD|a7N zEnkW{dE0sAe(oB+LheXIaCiN=e&tiKDC=OUx7(>Eqd#wSA@+CH@n=#Xy=TwLi?p3t zw_oO@9?P43D(ImSHkMR)pXZc+D&F4bBLiR{fu-=6pdtPith8I|K9<-?R(nZoPi;Bw zBVpD9SmA^I^h3bX9`w50f{3THsLPN0&P3H7`Z5-tC$UCJ@{y4)ROcQF=*LOL4--jB z@#Rh_xu!UL`WXoVG!4mKLyRy~X8>j7=qg3VTjgg_l)t>OkE5^w|u zAD(*rHn-6xFl6KaVO{L`Ru+ex^iv(V93Mczh4j}MmuOIyL#z?NMpy<*`efe&A_%Yj zAOY?u$*Noy+&T!!@B1VF+GdGliUbU#Z=E2Ri(Wi6&lmxOOnFLrkl{WC+Xq@iV3Bt= zzCqV=%#2PDb-5OKk}zThnUDo@IumP?$yB&l;C?Go=YOpmg6Ui+&rNt zsHi!OWGvBVfc%SQu;r}^lBcBU<_~L%loG;KC%O%RDNt4X{u~(tE|J~$V*{@RO7VRk zgGVGO5OR$8Fz(La%%$iY#)lR$fOm+y6_{0 z4rLnQ=fwD^?7qmAr>*g;1fCY8`9&Jv5O}sCX_k_B@*u!GNkd>KxuZcHdsl{YyyppJB|h;u&Bl?d}W$I_x*ee3JkX7VMk!zj($Y zT>5uN`&%3SAE@@HG6T9_$Dsf1=O0K0mRu5-ud{A4&EFe+8~SK%|ASy}{?lEyw}QUi zFa6xI@*AsO9rT1h@>x74m`ea6%vU@jE0i;>47QtArgju3<;am z?w_7&3S%6d@19svTY0g16SBU6gl?-oeRXK|%n$u;1G=^J@zbYw_t+fuUf!eMHZP{u z6jyJQM>Dc>&P%a8PB^s$%k}i@Ya(Gdq8Wx;T-0=MD&c)ojpgT4O7NAOpObW`<0L;F zbZ+=EE|EwShrqw%A9T!(8DS%lzcTWpM z?|8OJW^;mFR3CGdh|dgb@-~45V)R^+X99%L>M(%CX z&I~s=H@^a8+2&>9a%P`x3N7YoQLxbom2bSc0Lc=(exA`?n=N@XMJp_L^$kN;*WrrZ zN#Wd=!3#e!AhO z?R$0eLi+{U@Ml8KMY-I}AGGEy5kHwTVi>jtJ9t1es|XF+cwANDP38`&7zVg@CzG9; zspoMC2fJ%EMe;H;1t*3LKeK*%!7I2_%oozRP857N5{JP(8JUKSFJfW?9f18R$$>Jb z3R2=Q!!*&V*{bODcmfWeaaIBb%8Uz(z-E~_v&EtXt49PhxO>*zxbWvnz}7AZmj1j# zpU4aHhIz#0Umf^jP2mSQ^s*s~#MEys`H%oC8RTZ6W4iXrgZ}ivGOZ~7 zE0NkqGMmt#>&R$MCxY!lp-;xrn*$`H+;QYA234f%VX?1Gim%J~>LUt32X4W(nEgEy zVC=Zm1uUW^)H`zSH|j8D-ek9g&{Nif-h>)S7{R`LUoxS$B^M=KwRCJaPZ*LpP@snz z5|z2nL58WnCnJ40m-RQB;m0W9RE}@#{7M~u3SvrR&GH|sV^=X)QEJAF`jlQBsE=iz zA2!vA>cU={xF1Fi*KVUad;q!w8(G!_jcHDSq}M4ufQJ;n%#eW$f#nxy^wGAp0^%=OX$zRhKPd(v>~d7qpLz^HScp&j4eywL0~rVe zXtP*-mp@rn!H)YgG5cK%qo(=a8O=Ydtp6a@{yoJ+1LbUH)f=pv=7c47-T)M9w(Kg? zYuP<~-ow%Taz8}2vT?aZrYroZ#n6-Bh--2;ZbirV8b>BX7{#WfYM%q}EK)8exV zqU7_6!xRXG6`pbmCAGd~m5tuj#VGDN$J1)bZx4y0*lC4uD|H#N zeK5|Ug*&|4%ZU>lIhI7)zFvu&*)b4AMW1-?D(8wk3D0~THjCOM^9z?h%p_F5R!h^N z!cUFK2o{Cf_u82$XP5P~d0CueAQv|bT#%d^N}8+GvFqN4?P_(M!CDG}OQ|F+@cK?w z4y=zizHJY>MB4%nn6Yek)>LztV<;w&+FMrNi9W&nIqvwfS}mv+#Ln>dJ+_ z>u5sfqh893?+L4GO>3}AFCbkH6qM1Y9M)|^IA`t^njTfgT=#gX)uFSj!ubP!F#TmS z)8`^rsPir8zVD^Y3qLNf&*}Ik4um&e5DuAVzx9h!3a|@)f8qws1712t2GNCa8&$bo zEGN6Fu#=QKgVS+|2*&w7PJZH+B_rpzq|V|P!GLx3Xwd-v0e^`~nP^&ufn9SD`MGc` z2KL%AUGbtUA{wOpx*Y^nlc!QAXuUpE%7_Syh>SG~P>qC|_OFv@UEI#j(gU;IJS3H5 zG$E`Zj_j6aHj)iVZ1|%*vE)Wo;D0VcIf(F67UD0^W99CkgJ1l;1}aU^5x2ic1f7*+ zOoUrXvPPDuHM&7^dnEjX5QHTbWoErGD@gK3pylO{od90yu$V4cDeLWbC^22E^(Im~ z0qm;NRjf6m6O&sUq-OVK0k_1mok!7$3h&WS<%wQy$T4w(MydC;D3E_CTe_C0xL8h!D_(!y;&+xFiYNB{ z!c8>wY^~J}`*Olsfq3I~NA{y^`vVUS5wP9=#-<^w-hkMgKZ8;w|#-3*4>w z1wC`@Mc*56L&%d@R*!doO^w2=e)(_YvdbR@;1zH!^FN2z?c)+&v40>Jko7m8K>qnG z=s(qB|B#aXdu(z0%M1Hz|HhW*>>uLG53VD5r*6GHbW-xkll_oubB)XIhYG?TJ*6BI z9A%BdNPhCN5%CG?QpVA#?nY9HSp?a%JpZ`tLLXv&iJSc03deJ$H6G`(P}oYll-f2c zg#>_DBF%c|+aFn;kNDF@YD*WBC*`GYWB%}wn&&4bpY&g69GvYOnO9lfw!8*eqadN1 z%IB7Lp44APzo9@kMnC>Ey-Zz6DPyJNE8c!GV+&J|Ub$$H3;VWlNWwpkVA4IC9-Eq; za}_Of(RCoL3=Q2=ZXNgZGSs>q+bQQ*jVOAkP3Iu!Ba3s>JHC^6xU0JB!9Xgygl0b7 z1yO~RpQi4$icfu#jxsdU2B~JfN(aGO?Ona9E^7%nOFa;~Y+jlHg3id-Kl@yt(HXFa z^UP?Ils3Lvw9Ra>mf=9Or)^u#cUOP;xD6D@(2SS4*Xzu!a+jvE)y+1s&*w8;^aqbA zC-oy_)X^z-seH2-1ohD&abDkWRK9BH^IJ)i;4?NG=jD);p7}4FKDJsuo2g47*I#U& zJygDCZ8MZK@a@xu>tTUsKKz{L%8wsLiWo4ZfR|sBnsYLZr{nUcVi7 z;9#TQz9X~ApuoW)f(gO?mRN<6yY(dzQ76}xSwTo+&tnqlnVY7bCs*i~6!xRISS(0T zJ$;BIY7fEtNkpQ5W2@R6dfJ&zJ?dQa06tDI5{FMcO~R|B1cXs|(og4+T{-Q`VZ52> zyyneJH=vPWY5B{PD#ulGZs2wlZhzH_-$zT3U5^~{M|q;uR=CHCl&4kRY-|4j`agO* z)2OD-1Pq4|AYsvjMIs1>J*>u5c0m#ZK@ox#1j;HPpe#aN8pWCf0$~*i`@V-FXc7Vf zA`P1$i-1x@b|?YF2pCXt)H;`OPCIi>r$6SLnZNh`f6n_p_r2dYKM4zOd)!55?EG{X zyktlzYSdG)N=;Zh+m#uULO|0`M2Ad*FRQ*`c&bft0~r}Dd(KO@B@l$^t{Tp`nnby- zXCeEVxY{ziNTzP!JJ^w6S7jzQz}ew2$_=HnK-iai-90(2HFiFf0e+5M;p$%=G{-LL9dRN+YDxaJDLn z0GGI}5%%VZdCTi6!He~@>7lMrA8V$MXwQvCweVwc)@@hbzkYh^;9GqY1pZ+xe~u=B z^FPAUTx*K1urT;=&7=BMv(o>>*t38AQ|%{&WDuQJCXzXmOQ6`z0SB2oYZt?rV!PDb}2DfBbPX2pm(e*lAoYwC%mWmw1i zLLD5MxGuLsu3io6R=U6}YJtTrE!C7#@8G|WNEiQ-W@xkOj-BT@ z7SUHAO2PgxYWXoWqQ?Yb6ahz+tT|L<=(JTGcMYa$&!<`4gVZU*Kz@AN+-Opn+>5T) zw%4MU*q+iP@^r%W;FYl6?sXp`bU{*+uWmKg$rI_8Kl4ZOOg39v<(sHr#b9S?gBSEy0$}_@H?r+*VWjFURtJ&Y9`8%J9=wqg^`z_lTv%i+Muplwr}3rhn!F>TM^rHiDd~xd z+a@R~l7h;Nax0Ol`yg#@rOEOwxUv+*`E67ZY@=yJ%(!~o4yhncOo!Tfyr+VWuU*35 zKAE-LO>tfez-PKfu_#II;Q`=G&$GZdBaBKs4GYX05s67t;TDIgr@g46o#dl;v=`x5 zAMGi38yQ{StK%4JbUdXn3`?#IMF4jO6vU^Xs%1|=;p!D#NFmJtHOxeXxm}6&6!?a! z--?gg3gH~CGQ8%y8hf-nc$X)I8Y{x~ZX|fIBjLi?=l23%9n&Iw z{x(cKnRcZyoP_5RMpgzIE$+hbrahpGVKRsdbQ#gDYe3?PMda7(9a;q@6Ga?QvtLd= zLF7Q|@pSoT$|6EP3-DBs=+(wZp$5UmW3etCaIz*muxI8;eljq{89Q=fd?N*q*_6r>o>H7@LI* zHN2Z42{UQl!eYBjP$%ThZZF^moWE60KDLXCw45!Cn@Jb56c}qf__LT52gsu%KYy`n zoH)r^ILTUeVQg}|{jrG>c_l~q|~Kh@l+O}outvgl^{ zwT<_isQ1{dt!B;K_G04)4<9kjj63=Vs21HlBb8Jp0VE0WjZvHJ;FdVttO&In;6S9(xF672%P0V+YQ1cL|Bmy_( z2FnJ=?%C8IFoB|aNIX{rXm1o)IGy59bhLRD-=8LF`>x+;Q@!D|u+9tzNmgMMS|?v-OE+xMpkZBfq;LyKm5#$a~Bl&zE~VoSW+)zE+s;y<|c7 z zL@zNw5+&csDxbf6Ijod@VMyyU{r&m~-fzE?@+fiZ(W%eML(>SUZN+b2oI6r#S+2Z| zCkX1);sQ_@=?yB@iOoTT`#ET1%JqVZ}ugt)s0|80pg)X`Q;Wy|A4sX5|P&?W;_Zs8+=Kdtg{ z3QDgy@rS-yaJ_1WZnzqs8KQdS>3EnbBrdJ9EKll`5NAS;r*K`diy0gcZ?X{*1uN*M zu?sJ6bTE?+mjnhOw)=a3)!(~uR#cS2Gpw^^=C=yVFr#h;( zu~DGjFnwPZ8o8TCmdw9WOKXqCZD1cKsc`XwWo{QQo?tIvFWEsQFOHTRPY{6mRURiv zbUJ#$W%6e~SM>E++m|roy=1JQW52xMCkTyVnVX}=fB{qq*gW@63wkt{JHh2o?iZEw zfDH&2<1uVQA$4_}u#~&DvjK2D_PX&+M`rI4BWaF!86kVwjaus;rSa1kD#6kp^# zWuJT3%*lPIuElgHz-#c;y#xC9v7F`9zxg!3{|)*oXe6Dz&hfYX^d`pY=`S0_5HQI6 E@1&r|2mk;8 literal 0 HcmV?d00001 From 66c14ecf059a242fa8dc3a596243625124d33612 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Mon, 20 Nov 2023 13:16:04 +0000 Subject: [PATCH 2/3] feat: Use transparent background on animations Note that we use `DisposalRestoreBackground` in encoding the GIF, whereas previously we indirectly used `diagrams-rasterific`'s default, `DisposalAny`. While that worked with the default black background, retaining it now would mean that we would see previous frames remaining in the background rather than disappearing. Signed-off-by: George Thomas --- primer/primer.cabal | 1 + primer/src/Primer/Primitives.hs | 28 +++++++++++++++-------- primer/test/outputs/eval/animation/1.gif | Bin 3199 -> 3313 bytes primer/test/outputs/eval/animation/2.gif | Bin 52327 -> 52888 bytes 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/primer/primer.cabal b/primer/primer.cabal index 2eb3a2e87..bdde7d40d 100644 --- a/primer/primer.cabal +++ b/primer/primer.cabal @@ -117,6 +117,7 @@ library , 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 diff --git a/primer/src/Primer/Primitives.hs b/primer/src/Primer/Primitives.hs index 0a54ba03d..6d592227d 100644 --- a/primer/src/Primer/Primitives.hs +++ b/primer/src/Primer/Primitives.hs @@ -28,17 +28,24 @@ module Primer.Primitives ( 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 ( - GifLooping (LoopingForever), - defaultPaletteOptions, - rasterGif, + Options (RasterificOptions), + Rasterific (Rasterific), ) import Diagrams.Prelude ( + Diagram, V2 (..), circle, deg, @@ -48,6 +55,7 @@ import Diagrams.Prelude ( mkSizeSpec, rect, rectEnvelope, + renderDia, rotate, sRGB24, translate, @@ -343,7 +351,7 @@ primFunDef def args = case def 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 <- traverse diagramAtTime [0 .. (duration * 100) `div` frameLength - 1] -> + | Just (frames :: [Diagram Rasterific]) <- traverse diagramAtTime [0 .. (duration * 100) `div` frameLength - 1] -> Right $ prim $ PrimAnimation @@ -354,12 +362,14 @@ primFunDef def args = case def of -- 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 + $ encodeComplexGifImage + $ GifEncode (fromInteger width) (fromInteger height) Nothing Nothing gifLooping + $ flip palettizeWithAlpha DisposalRestoreBackground $ map - ( (,fromInteger frameLength) + ( (fromInteger frameLength,) + . renderDia + Rasterific + (RasterificOptions (mkSizeSpec $ Just . fromInteger <$> V2 width height)) . rectEnvelope (fromInteger <$> mkP2 (-width `div` 2) (-height `div` 2)) (fromInteger <$> V2 width height) diff --git a/primer/test/outputs/eval/animation/1.gif b/primer/test/outputs/eval/animation/1.gif index 37a307b8a0cb3b9160a37c0730759e466ccf024f..534df5ee511c9e86d4de5bd3c66c0d9cdd2d958e 100644 GIT binary patch literal 3313 zcmZ?wbhEHbT)+^;P|m=h_@CR)H6+;CF~HSG&w!bc0Vwp7g_Dbci9rX*W&kN`1Tp@D z2q0kkKc#==>9_og=WMyvz4_jr-~4TlJf=PCT=weJws+jeKlxn$*8BX|x$pn@dAQn- zEc)2t^Ns1uvd@wMUJI{eg?MerS{d=++v&Hj4m{bgBh1hKyz$oRvt@gqpS9@j+kead z=bw95u77tIdSNaRq*3u=qQs6H7gzk4A^6gwGf=bAb7JOB%Zn?2{uFp+`6@uGYS)CU z9j7j=`jsW{+NwLCyL|No@9ncMSby)9eq&Y>kW-^GVa>KPcVfQD=I==>Ry>(FdF6x!H50E)c(&x!RGsKuHJrxF&dfB)zO|>* tc=@@xI$WdSI2w+l>2Wk)kCu<4<>P2QGg@DdwtGg~kE89!foO+Y0{|J`f^7f* delta 317 zcmew;`Cr1?-P6s&GI0Sz6vGDwF!;})_@CR)H6+;CF~HSG&w!Z`D6IICg@Fr5>wuJi Z6f-dWZ<*-0l96ZP>jt3_CsoU64FFxtOEUlf diff --git a/primer/test/outputs/eval/animation/2.gif b/primer/test/outputs/eval/animation/2.gif index 508342fb4e16e6b1db4de10667c390dca025ee42..e92378997d80297808e9ac1ac3171420e40fddd7 100644 GIT binary patch delta 4416 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 STqGv{>%9Z$1F`Ii1pf;uZ+nmc From 3897c97c5dbb0b720803bcaf5546e005181a9345 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Tue, 21 Nov 2023 01:13:53 +0000 Subject: [PATCH 3/3] refactor: Clarify primitive pattern typechecking logic Signed-off-by: George Thomas --- primer/src/Primer/Typecheck.hs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/primer/src/Primer/Typecheck.hs b/primer/src/Primer/Typecheck.hs index 0ee262698..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. @@ -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 || tc == tAnimation) $ 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