packages feed

ginger2-2.0.0.0: src/Language/Ginger/AST.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}

module Language.Ginger.AST
where

import Data.Aeson (ToJSON (..), ToJSONKey (..), FromJSON (..), FromJSONKey (..))
import Data.List (intercalate)
import Data.Maybe (maybeToList)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.String (IsString (..))
import Data.Text (Text, pattern (:<) )
import qualified Data.Text as Text
import Data.Vector (Vector)
import qualified Data.Vector as V
import Test.Tasty.QuickCheck (Arbitrary (..))
import qualified Test.Tasty.QuickCheck as QC

import Language.Ginger.SourcePosition

-- | Identifiers are used to represent variable names and object fields.
newtype Identifier =
  Identifier { identifierName :: Text }
  deriving (Show, Eq, Ord, ToJSON, ToJSONKey, FromJSON, FromJSONKey)

instance IsString Identifier where
  fromString = Identifier . Text.pack

instance Arbitrary Identifier where
  arbitrary = do
    x <- QC.oneof $ map pure identifierLeadChars
    xs <- QC.listOf (QC.oneof $ map pure identifierChars)
    pure $ Identifier $ Text.pack (x : xs)
  shrink _ = [] -- shrinking identifiers breaks references

isValidIdentifier :: Text -> Bool
isValidIdentifier t =
  case t of
    Text.Empty -> False
    c :< _ -> c `elem` identifierLeadChars

identifierLeadChars :: [Char]
identifierLeadChars =
  [ 'a' .. 'z' ] ++ ['A' .. 'Z'] ++ ['_']

identifierChars :: [Char]
identifierChars =
  identifierLeadChars ++ ['0' .. '9']

-- | Represents an encoded string value, as opposed to a raw (unencoded) string,
-- which we represent as a plain 'Text'.
newtype Encoded =
  Encoded { encoded :: Text }
  deriving (Show, Eq, Ord, Semigroup, Monoid)

instance Arbitrary Encoded where
  arbitrary = Encoded . Text.replace "{" "{ " . Text.pack <$> QC.listOf arbitrary
  shrink (Encoded e) =
    map (Encoded . Text.pack) . filter (not . null) $ shrink $ Text.unpack e

-- | A template consists of an optional parent template (specified in the
-- source using the @{% extends %}@ construct), and a body statement.
data Template =
  Template
    { templateParent :: !(Maybe Text)
    , templateBody :: !Statement
    }
    deriving (Show, Eq)

-- | A block represents a section of a template that can be overridden in
-- derived templates ("template inheritance").
data Block =
  Block
    { blockBody :: !Statement
    , blockScoped :: !Scoped
    , blockRequired :: !Required
    }
    deriving (Show, Eq)

data SetTarget
  = SetVar !Identifier
  | SetMutable !Identifier !Identifier
  deriving (Show, Eq)

-- | A statement in the template language.
data Statement
  = -- | Statement tagged with a source position
    PositionedS !SourcePosition !Statement
  | -- | Bare text written in the template, outside of any curly braces
    ImmediateS !Encoded
  | -- | An expression interpolation: @{{ expr }}@
    InterpolationS !Expr
  | -- | Comment: @{# comment text #}@
    CommentS !Text
  | -- | @@@
    --   {% for keyVar, valueVar in iteree if loopCondition recursive %}
    --   body
    --   {% else %}
    --   body if empty
    --   {% endfor %}
    --   @@@
    ForS
      !(Maybe Identifier) -- optional loop key variable
      !Identifier -- loop element variable
      !Expr -- iteree
      !(Maybe Expr) -- optional loop condition
      !Recursivity -- enable recursion?
      !Statement -- loop body
      !(Maybe Statement) -- else branch in case iteree is empty
  | -- | @{% if condition %}yes branch{% else %}no branch{% endif %}@
    IfS
      !Expr -- condition
      !Statement -- true branch
      !(Maybe Statement) -- false branch
  | -- | @{% macro name(args) %}body{% endmacro %}@
    MacroS
      !Identifier -- macro name
      ![MacroArg] -- arguments
      !Statement -- body
  | -- | @{% call macroName(args) %}body{% endcall %}@
    CallS
      !Identifier -- callee
      ![Expr] -- positional args
      ![(Identifier, Expr)] -- keyword args
      !Statement -- body (@caller()@)
  | -- | @{% filter filterName(args, kwargs) %}body{% endfilter %}@
    FilterS
      !Identifier -- name
      ![Expr] -- positional args
      ![(Identifier, Expr)] -- keyword args
      !Statement -- body
  | -- | @{% set name=expr %}@
    SetS
      !SetTarget -- variable name
      !Expr -- value
  | -- | @{% set name %}body{% endset %}@
    SetBlockS
      !SetTarget -- variable name
      !Statement -- body
      !(Maybe Expr) -- optional filter
  | -- | @{% include includee ignore missing with context %}@
    IncludeS
      !Expr
      !IncludeMissingPolicy
      !IncludeContextPolicy
  | -- | @{% import importee as localName item, other_item as other ignore missing with context %}@
    ImportS
      !Expr -- filename
      !(Maybe Identifier) -- local name
      !(Maybe [(Identifier, Maybe Identifier)]) -- [ (imported name, local name) ]
      !IncludeMissingPolicy !IncludeContextPolicy
  | -- | @{% block name with scope required %}body{% endblock %}@
    BlockS
      !Identifier -- block name
      !Block
  | -- | @{% with defs %}body{% endwith %}@
    WithS
      ![(Identifier, Expr)]
      !Statement
  | -- | Group of statements; not parsed, but needed for combining statements
    -- sequentially.
    GroupS ![Statement]
  deriving (Show, Eq)

data IncludeMissingPolicy
  = RequireMissing
  | IgnoreMissing
  deriving (Show, Eq, Ord, Enum, Bounded)

data IncludeContextPolicy
  = WithContext
  | WithoutContext
  deriving (Show, Eq, Ord, Enum, Bounded)

escapeComment :: Statement -> Statement
escapeComment (CommentS txt) =
  case Text.splitOn "#}" txt of
    [] -> GroupS []
    [x] -> CommentS x
    xs -> GroupS . intercalate [CommentS "#", CommentS "}"] . map ((:[]) . CommentS) $ xs
escapeComment x = x

instance Arbitrary Statement where
  arbitrary = arbitraryStatement mempty
  shrink (PositionedS pos s) =
    PositionedS pos <$> shrink s ++
    [s]
  shrink (GroupS xs) = map GroupS $ shrink xs
  shrink (ImmediateS txt) = map ImmediateS $ shrink txt
  shrink (InterpolationS e) = map InterpolationS $ shrink e
  shrink (CommentS txt) = map (escapeComment . CommentS . Text.pack) $ shrink $ Text.unpack txt
  shrink (ForS keyMay val iteree condMay recur body elseBranchMay) =
    (ForS <$> pure keyMay <*> pure val <*> pure iteree <*> pure condMay <*> pure recur <*> pure body <*> shrink elseBranchMay) ++
    (ForS <$> pure keyMay <*> pure val <*> pure iteree <*> pure condMay <*> pure recur <*> shrink body <*> pure elseBranchMay) ++
    (ForS <$> pure keyMay <*> pure val <*> pure iteree <*> pure condMay <*> shrink recur <*> pure body <*> pure elseBranchMay) ++
    (ForS <$> pure keyMay <*> pure val <*> pure iteree <*> shrink condMay <*> pure recur <*> pure body <*> pure elseBranchMay) ++
    (ForS <$> pure keyMay <*> pure val <*> shrink iteree <*> pure condMay <*> pure recur <*> pure body <*> pure elseBranchMay) ++
    [body] ++
    maybe [] (:[]) elseBranchMay
  shrink (IfS cond yes noMay) =
    (IfS <$> shrink cond <*> pure yes <*> pure noMay) ++
    (IfS <$> pure cond <*> shrink yes <*> pure noMay) ++
    (IfS <$> pure cond <*> pure yes <*> shrink noMay) ++
    (pure yes) ++
    (maybeToList noMay)
  shrink (MacroS name args body) =
    (MacroS <$> pure name <*> pure args <*> shrink body) ++
    (MacroS <$> pure name <*> shrink args <*> pure body) ++
    (MacroS <$> shrink name <*> shrink args <*> shrink body) ++
    [body]
  shrink (CallS name args kwargs body) =
    (CallS <$> pure name <*> pure args <*> pure kwargs <*> shrink body) ++
    (CallS <$> pure name <*> pure args <*> shrink kwargs <*> shrink body) ++
    (CallS <$> pure name <*> shrink args <*> pure kwargs <*> pure body) ++
    [body]
  shrink (FilterS name args kwargs body) =
    (FilterS <$> pure name <*> pure args <*> pure kwargs <*> shrink body) ++
    (FilterS <$> pure name <*> pure args <*> shrink kwargs <*> shrink body) ++
    (FilterS <$> pure name <*> shrink args <*> pure kwargs <*> pure body) ++
    [body]
  shrink (SetS name expr) =
    (SetS <$> pure name <*> shrink expr)
  shrink (SetBlockS name body filterMay) =
    (SetBlockS <$> pure name <*> pure body <*> shrink filterMay) ++
    (SetBlockS <$> pure name <*> shrink body <*> pure filterMay)
  shrink (IncludeS name m c) =
    (IncludeS <$> pure name <*> pure m <*> shrink c) ++
    (IncludeS <$> pure name <*> shrink m <*> pure c)
  shrink (ImportS name lname imports m c) =
    (ImportS <$> pure name <*> pure lname <*> pure imports <*> pure m <*> shrink c) ++
    (ImportS <$> pure name <*> pure lname <*> pure imports <*> shrink m <*> pure c) ++
    (ImportS <$> pure name <*> pure lname <*> shrink imports <*> pure m <*> pure c) ++
    (ImportS <$> pure name <*> shrink lname <*> pure imports <*> pure m <*> pure c)
  shrink (BlockS name (Block b s r)) =
    (BlockS <$> pure name <*> (Block <$> pure b <*> pure s <*> shrink r)) ++
    (BlockS <$> pure name <*> (Block <$> pure b <*> pure s <*> shrink r)) ++
    [b]
  shrink (WithS defs body) =
    (WithS <$> pure defs <*> shrink body) ++
    (WithS <$> shrink defs <*> pure body) ++
    [body]

instance Arbitrary IncludeMissingPolicy where
  arbitrary = QC.oneof $ map pure [minBound .. maxBound]

instance Arbitrary IncludeContextPolicy where
  arbitrary = QC.oneof $ map pure [minBound .. maxBound]

arbitraryStatement :: Set Identifier -> QC.Gen Statement
arbitraryStatement defined = do
    fuel <- QC.getSize
    QC.resize (max 0 $ fuel - 1) $
      QC.oneof
        [ ImmediateS <$> arbitrary
        , InterpolationS <$> arbitraryExpr defined
        , escapeComment . CommentS . Text.strip . Text.pack <$> arbitrary

        , do
            let fuel' = fuel `div` 6
            keyNameMaybe <- arbitrary
            valName <- arbitrary
            iteree <- QC.resize fuel' $ arbitraryExpr defined
            let defined' = mempty <> Set.singleton valName <> Set.fromList (maybeToList keyNameMaybe) <> Set.singleton "loop"
            filterMay <- QC.oneof [ pure Nothing, QC.resize fuel' $ Just <$> arbitraryExpr defined ]
            body <- QC.resize fuel' $ arbitraryStatement defined'
            elseBody <- QC.resize fuel' $ QC.oneof [ pure Nothing, Just <$> arbitraryStatement defined' ]
            recursive <- arbitrary

            pure $ ForS keyNameMaybe valName iteree filterMay recursive body elseBody

        , IfS <$> QC.resize (fuel `div` 3) (arbitraryExpr defined)
              <*> QC.resize (fuel `div` 3) (arbitraryStatement defined)
              <*> QC.resize (fuel `div` 3) (QC.oneof [ pure Nothing, Just <$> arbitraryStatement defined ])

        , do
            args <- QC.resize (fuel `div` 2) $
                      QC.listOf ((,) <$> arbitrary <*> QC.oneof [ pure Nothing, Just <$> arbitraryExpr mempty ])
            let defined' = Set.fromList (map fst args)
            body <- QC.resize (fuel `div` 2) (arbitraryStatement defined')
            name <- arbitrary
            pure $ MacroS name args body
        , CallS <$> arbitrary
                 <*> QC.resize (fuel `div` 4) (QC.listOf (arbitraryExpr defined))
                 <*> QC.resize (fuel `div` 4) (QC.listOf ((,) <$> arbitrary <*> arbitraryExpr defined))
                 <*> QC.resize (fuel `div` 2) (arbitraryStatement $ defined <> Set.singleton "caller")
        , FilterS <$> arbitrary
                  <*> QC.resize (fuel `div` 3) (QC.listOf (arbitraryExpr defined))
                  <*> QC.resize (fuel `div` 3) (QC.listOf ((,) <$> arbitrary <*> arbitraryExpr defined))
                  <*> QC.resize (fuel `div` 3) (arbitraryStatement defined)
        , SetS <$> QC.frequency
                      [ (100, SetVar <$> arbitrary)
                      , (5, SetMutable <$> arbitrary <*> arbitrary)
                      ]
               <*> QC.resize (fuel `div` 2) (arbitraryExpr defined)
        , SetBlockS <$> QC.frequency
                          [ (100, SetVar <$> arbitrary)
                          , (5, SetMutable <$> arbitrary <*> arbitrary)
                          ]
                    <*> QC.resize (fuel `div` 2) (arbitraryStatement defined)
                    <*> QC.resize (fuel `div` 2) (QC.oneof [ pure Nothing, Just <$> arbitraryExpr defined ])

        -- , IncludeS <$> arbitraryExpr defined
        -- , BlockS <$> arbitrary
        --          <*> QC.resize (fuel `div` 2) (arbitraryStatement defined)
        --          <*> arbitrary
        --          <*> arbitrary

        , do
            vars <- QC.listOf1 ((,) <$> arbitrary <*> QC.resize (fuel `div` 4) (arbitraryExpr defined))
            let defined' = mempty <> Set.fromList (map fst vars)
            body <- QC.resize (fuel * 3 `div` 4) (arbitraryStatement defined')
            pure $ WithS vars body
        ]

class Boolish a where
  is :: a -> Bool

isNot :: Boolish a => a -> Bool
isNot = not . is

data Scoped = NotScoped | Scoped
  deriving (Show, Read, Eq, Ord, Enum, Bounded)

instance Boolish Scoped where
  is = (== Scoped)

data Required = Optional | Required
  deriving (Show, Read, Eq, Ord, Enum, Bounded)

instance Boolish Required where
  is = (== Required)

data Recursivity = NotRecursive | Recursive
  deriving (Show, Read, Eq, Ord, Enum, Bounded)

instance Boolish Recursivity where
  is = (== Recursive)

instance Arbitrary Scoped where
  arbitrary = QC.oneof $ map pure [minBound..maxBound]

instance Arbitrary Required where
  arbitrary = QC.oneof $ map pure [minBound..maxBound]

instance Arbitrary Recursivity where
  arbitrary = QC.oneof $ map pure [minBound..maxBound]

type MacroArg = (Identifier, Maybe Expr)

-- | An expression. Expressions can occur in interpolations (@{{ ... }}@), and
-- in various places inside statements.
data Expr
  = PositionedE !SourcePosition !Expr
  | NoneE
  | BoolE !Bool
  | StringLitE !Text
  | IntLitE !Integer
  | FloatLitE !Double
  | StatementE !Statement
  | ListE !(Vector Expr)
  | DictE ![(Expr, Expr)]
    -- | @UnaryE op rhs
  | UnaryE !UnaryOperator !Expr
    -- | @BinaryE op lhs rhs
  | BinaryE !BinaryOperator !Expr !Expr
    -- | @SliceE slicee start length
  | SliceE !Expr !(Maybe Expr) !(Maybe Expr)
    -- | @DotE lhs rhs
  | DotE !Expr !Identifier
    -- | @IsE scrutinee test args kwargs@
  | IsE !Expr !Expr ![Expr] ![(Identifier, Expr)]
    -- | @CallE callee args kwargs@
  | CallE !Expr ![Expr] ![(Identifier, Expr)]
    -- | @FilterE arg0 filter args kwargs@
  | FilterE !Expr !Expr ![Expr] ![(Identifier, Expr)]
    -- | @TernaryE cond yes no@
  | TernaryE !Expr !Expr !Expr
  | VarE !Identifier
  deriving (Show, Eq)

pattern TrueE :: Expr
pattern TrueE = BoolE True

pattern FalseE :: Expr
pattern FalseE = BoolE False

instance Arbitrary Expr where
  arbitrary = arbitraryExpr mempty
  shrink (PositionedE pos e) =
    PositionedE pos <$> shrink e ++
    [e]
  shrink (StringLitE txt) = map (StringLitE . Text.pack) $ shrink $ Text.unpack txt
  shrink (IntLitE i) = IntLitE <$> shrink i
  shrink (FloatLitE i) = FloatLitE <$> shrink i
  shrink (StatementE s) = StatementE <$> shrink s
  shrink (ListE v) =
    case V.uncons v of
      Nothing -> pure NoneE
      Just (x, xs) | V.null xs ->
        (ListE . V.singleton <$> shrink x) ++ [x]
      _ ->
        ListE . V.fromList <$> shrink (V.toList v)
  shrink (DictE xs) = DictE <$> shrink xs
  shrink (UnaryE op e) =
    (UnaryE <$> pure op <*> shrink e) ++
    [e]
  shrink (DotE a b) =
    (DotE <$> pure a <*> shrink b) ++
    (DotE <$> shrink a <*> pure b) ++
    [a]
  shrink (BinaryE op a b) =
    (BinaryE <$> pure op <*> shrink a <*> pure b) ++
    (BinaryE <$> pure op <*> pure a <*> shrink b) ++
    [a, b]
  shrink (SliceE slicee startMay endMay) =
    (SliceE <$> pure slicee <*> pure startMay <*> shrink endMay) ++ 
    (SliceE <$> pure slicee <*> shrink startMay <*> pure endMay) ++ 
    (SliceE <$> shrink slicee <*> pure startMay <*> pure endMay) ++ 
    maybeToList startMay ++
    maybeToList endMay ++
    [slicee]
  shrink (IsE a b args kwargs) =
    (IsE <$> pure a <*> pure b <*> pure args <*> shrink kwargs) ++
    (IsE <$> pure a <*> pure b <*> shrink args <*> pure kwargs) ++
    (IsE <$> pure a <*> shrink b <*> pure args <*> pure kwargs) ++
    (IsE <$> shrink a <*> pure b <*> pure args <*> pure kwargs) ++
    [a, b] ++ args ++ map snd kwargs
  shrink (CallE f args kwargs) =
    (CallE <$> pure f <*> pure args <*> shrink kwargs) ++
    (CallE <$> pure f <*> shrink args <*> pure kwargs) ++
    (CallE <$> shrink f <*> pure args <*> pure kwargs) ++
    [f] ++ args ++ map snd kwargs
  shrink (FilterE a f args kwargs) =
    (FilterE <$> pure a <*> pure f <*> pure args <*> shrink kwargs) ++
    (FilterE <$> pure a <*> pure f <*> shrink args <*> pure kwargs) ++
    (FilterE <$> pure a <*> shrink f <*> pure args <*> pure kwargs) ++
    (FilterE <$> shrink a <*> pure f <*> pure args <*> pure kwargs) ++
    [a, f] ++ args ++ map snd kwargs
  shrink (TernaryE a b c) =
    (TernaryE <$> pure a <*> pure b <*> shrink c) ++
    (TernaryE <$> pure a <*> shrink b <*> pure c) ++
    (TernaryE <$> shrink a <*> pure b <*> pure c) ++
    [a, b, c]
  shrink _ = []

arbitraryExpr :: Set Identifier -> QC.Gen Expr
arbitraryExpr defined = do
  fuel <- QC.getSize
  if fuel <= 1 then
    pure NoneE
  else do
    let baseOptions =
          [ (10, pure NoneE)
          , (100, BoolE <$> arbitrary)
          , (100, StringLitE <$> QC.resize (fuel - 1) (Text.pack <$> QC.listOf arbitrary))
          , (100, IntLitE <$> QC.resize (fuel - 1) arbitrary)
          , (100, FloatLitE <$> QC.resize (fuel - 1) arbitrary)
          , (100, ListE . V.fromList <$> fuelledList (arbitraryExpr defined))
          , (100, DictE <$> fuelledList ((,) <$> arbitraryExpr defined <*> arbitraryExpr defined))
          , (90, QC.resize (max 0 $ fuel `div` 2 - 1) $
              BinaryE <$> arbitrary
                      <*> arbitraryExpr defined
                      <*> arbitraryExpr defined
            )

          , (10, QC.resize (max 0 $ fuel `div` 2 - 1) $
              SliceE <$> arbitraryExpr defined
                     <*> (fmap IntLitE <$> arbitrary)
                     <*> (fmap IntLitE <$> arbitrary)
            )
          , (10, QC.resize (max 0 $ fuel `div` 2 - 1) $
              DotE <$> arbitraryExpr defined
                   <*> arbitrary
            )

          , (100, QC.resize (fuel `div` 3) $
              CallE <$> arbitraryExpr defined
                    <*> fuelledList (arbitraryExpr defined)
                    <*> fuelledList ((,) <$> arbitrary <*> arbitraryExpr defined)
            )

          , (100, QC.resize (fuel `div` 3) $
              TernaryE <$> arbitraryExpr defined
                       <*> arbitraryExpr defined
                       <*> arbitraryExpr defined
            )

          , (10, QC.resize (fuel - 1) $
              VarE <$> arbitrary
            )
          ]
        extraOptions =
          if Set.null defined then
            []
          else
            [ (100, VarE <$> QC.oneof (map pure $ Set.toList defined))
            ]
        options = baseOptions <> extraOptions
    QC.frequency options

data UnaryOperator
  = UnopNot
  | UnopNegate
  deriving (Show, Eq, Enum, Ord, Bounded)

pattern NotE :: Expr -> Expr
pattern NotE a = UnaryE UnopNot a

pattern NegateE :: Expr -> Expr
pattern NegateE a = UnaryE UnopNegate a

data BinaryOperator
    -- Math
  = BinopPlus
  | BinopMinus
  | BinopDiv
  | BinopIntDiv
  | BinopMod
  | BinopMul
  | BinopPower
    -- Comparison
  | BinopEqual
  | BinopNotEqual
  | BinopGT
  | BinopGTE
  | BinopLT
  | BinopLTE
    -- Boolean
  | BinopAnd
  | BinopOr

    -- List / dict membership
  | BinopIn
    -- List / dict indexing (@[]@)
  | BinopIndex
    -- String concatenation
  | BinopConcat
    -- Test
  deriving (Show, Eq, Enum, Ord, Bounded)

pattern PlusE :: Expr -> Expr -> Expr
pattern PlusE a b = BinaryE BinopPlus a b

pattern MinusE :: Expr -> Expr -> Expr
pattern MinusE a b = BinaryE BinopMinus a b

pattern DivE :: Expr -> Expr -> Expr
pattern DivE a b = BinaryE BinopDiv a b

pattern IntDivE :: Expr -> Expr -> Expr
pattern IntDivE a b = BinaryE BinopIntDiv a b

pattern ModE :: Expr -> Expr -> Expr
pattern ModE a b = BinaryE BinopMod a b

pattern MulE :: Expr -> Expr -> Expr
pattern MulE a b = BinaryE BinopMul a b

pattern PowerE :: Expr -> Expr -> Expr
pattern PowerE a b = BinaryE BinopPower a b

pattern EqualE :: Expr -> Expr -> Expr
pattern EqualE a b = BinaryE BinopEqual a b

pattern NotEqualE :: Expr -> Expr -> Expr
pattern NotEqualE a b = BinaryE BinopNotEqual a b

pattern GT_E :: Expr -> Expr -> Expr
pattern GT_E a b = BinaryE BinopGT a b

pattern GTE_E :: Expr -> Expr -> Expr
pattern GTE_E a b = BinaryE BinopGTE a b

pattern LT_E :: Expr -> Expr -> Expr
pattern LT_E a b = BinaryE BinopLT a b

pattern LTE_E :: Expr -> Expr -> Expr
pattern LTE_E a b = BinaryE BinopLTE a b

pattern AndE :: Expr -> Expr -> Expr
pattern AndE a b = BinaryE BinopAnd a b

pattern OrE :: Expr -> Expr -> Expr
pattern OrE a b = BinaryE BinopOr a b

pattern InE :: Expr -> Expr -> Expr
pattern InE a b = BinaryE BinopIn a b

pattern IndexE :: Expr -> Expr -> Expr
pattern IndexE a b = BinaryE BinopIndex a b

pattern ConcatE :: Expr -> Expr -> Expr
pattern ConcatE a b = BinaryE BinopConcat a b

instance Arbitrary BinaryOperator where
  arbitrary = QC.oneof (map pure [minBound .. maxBound])

fuelledList :: QC.Gen a -> QC.Gen [a]
fuelledList subGen = do
  fuel <- QC.getSize
  if fuel < 1 then
    pure []
  else do
    s <- QC.chooseInt (1, fuel)
    x <- QC.resize s subGen
    xs <- QC.resize (max 0 $ fuel - s - 10) (fuelledList subGen)
    pure $ x : xs


traverseS :: (Statement -> Statement) -> (Expr -> Expr) -> Statement -> Statement
traverseS fS fE stmt = go (fS stmt)
  where
    go (PositionedS pos s) = PositionedS pos $ traverseS fS fE s
    go (GroupS xs) = GroupS (map (traverseS fS fE) xs)
    go (InterpolationS e) = InterpolationS (traverseE fE fS e)
    go (ForS k v i condMay rec body elseMay) =
      ForS
        k v
        (traverseE fE fS i)
        (traverseE fE fS <$> condMay)
        rec
        (traverseS fS fE body)
        (traverseS fS fE <$> elseMay)
    go (IfS cond yes noMay) =
      IfS (traverseE fE fS cond) (traverseS fS fE yes) (traverseS fS fE <$> noMay)
    go (MacroS name args body) =
      MacroS name [(n, traverseE fE fS <$> d) | (n, d) <- args] (traverseS fS fE body)
    go (CallS name args kwargs body) =
      CallS
        name
        (traverseE fE fS <$> args)
        [(n, traverseE fE fS v) | (n, v) <- kwargs]
        (traverseS fS fE body)
    go (FilterS name args kwargs body) =
      FilterS
        name
        (traverseE fE fS <$> args)
        [(n, traverseE fE fS v) | (n, v) <- kwargs]
        (traverseS fS fE body)
    go (SetS name val) =
      SetS name (traverseE fE fS val)
    go (SetBlockS name body filterMay) =
      SetBlockS name (traverseS fS fE body) (traverseE fE fS <$> filterMay)
    go (IncludeS includee mp cp) =
      IncludeS (traverseE fE fS includee) mp cp
    go (ImportS importee lname imports mp cp) =
      ImportS (traverseE fE fS importee) lname imports mp cp
    go (BlockS name (Block body s r)) =
      BlockS name (Block (traverseS fS fE body) s r)
    go (WithS defs body) =
      WithS [ (n, traverseE fE fS e) | (n, e) <- defs ] (traverseS fS fE body)
    go s = s

traverseE :: (Expr -> Expr) -> (Statement -> Statement) -> Expr -> Expr
traverseE fE fS expr = go (fE expr)
  where
    go (StatementE s) = StatementE (traverseS fS fE s)
    go (ListE xs) = ListE (fmap (traverseE fE fS) xs)
    go (DictE items) =
      DictE [ (traverseE fE fS k, traverseE fE fS v) | (k, v) <- items ]
    go (UnaryE op e) = UnaryE op (traverseE fE fS e)
    go (BinaryE op a b) = BinaryE op (traverseE fE fS a) (traverseE fE fS b)
    go (SliceE slicee startMay lengthMay) =
      SliceE
        (traverseE fE fS slicee)
        (traverseE fE fS <$> startMay)
        (traverseE fE fS <$> lengthMay)
    go (DotE e i) = DotE (traverseE fE fS e) i
    go (IsE scrutinee test args kwargs) =
      IsE
        (traverseE fE fS scrutinee)
        (traverseE fE fS test)
        (traverseE fE fS <$> args)
        [ (n, traverseE fE fS e) | (n, e) <- kwargs ]
    go (CallE callee args kwargs) =
      CallE
        (traverseE fE fS callee)
        (traverseE fE fS <$> args)
        [ (n, traverseE fE fS e) | (n, e) <- kwargs ]
    go (FilterE arg0 f args kwargs) =
      FilterE
        (traverseE fE fS arg0)
        (traverseE fE fS f)
        (traverseE fE fS <$> args)
        [ (n, traverseE fE fS e) | (n, e) <- kwargs ]
    go (TernaryE cond yes no) =
      TernaryE
        (traverseE fE fS cond)
        (traverseE fE fS yes)
        (traverseE fE fS no)
    go e = e