packages feed

postgresql-syntax-0.5.0.0: library-internal/PostgresqlSyntax/Ast/SelectWithParens.hs

module PostgresqlSyntax.Ast.SelectWithParens
  ( SelectWithParens (..),
    refineToSelectWithParens,
    withParensSelectWithParens,
  )
where

import qualified HeadedMegaparsec as Parser
import {-# SOURCE #-} PostgresqlSyntax.Ast.SelectNoParens (SelectNoParens)
import {-# SOURCE #-} qualified PostgresqlSyntax.Ast.SelectNoParens as SelectNoParens
import qualified PostgresqlSyntax.Helpers.Gens as Gens
import qualified PostgresqlSyntax.Helpers.Parsers as Parsers
import qualified PostgresqlSyntax.Helpers.TextBuilders as TextBuilders
import PostgresqlSyntax.IsAst
import PostgresqlSyntax.Prelude
import qualified Test.QuickCheck as Qc

-- |
-- ==== References
-- @
-- select_with_parens:
--   |  '(' select_no_parens ')'
--   |  '(' select_with_parens ')'
-- @
data SelectWithParens
  = NoParensSelectWithParens SelectNoParens
  | WithParensSelectWithParens SelectWithParens
  deriving (Show, Generic, Eq, Ord, Data)

instance IsAst SelectWithParens where
  toTextBuilder settings =
    TextBuilders.renderInParens . \case
      NoParensSelectWithParens a -> toTextBuilder settings a
      WithParensSelectWithParens a -> toTextBuilder settings a

  -- @gram.y@ gives two productions, @'(' select_with_parens ')'@ and
  -- @'(' select_no_parens ')'@, and they overlap: a @select_no_parens@ may
  -- itself be nothing but a @select_clause@, and a @select_clause@ may
  -- itself be a @select_with_parens@. Transcribed literally as two
  -- alternatives, that overlap makes every nested paren group get parsed
  -- twice — once down the @select_with_parens@ branch and once down the
  -- @select_no_parens@ branch — so the cost doubles with each level of
  -- nesting. So the shared prefix is parsed once and classified afterwards.
  --
  -- ==== Canonical shape
  --
  -- Because the productions overlap, @((select 1))@ has two
  -- representations: @WithParensSelectWithParens@ wrapping the inner
  -- parenthesised select, or @NoParensSelectWithParens@ of a
  -- @SelectNoParens@ whose clause is that same inner parenthesised select.
  -- Both render back to the same text. __The first is canonical.__
  parser settings = Parsers.inParens selectWithParensBody
    where
      selectWithParensBody =
        asum
          [ do
              a <- Parser.wrapToHead (parser settings)
              either WithParensSelectWithParens NoParensSelectWithParens <$> SelectNoParens.afterSelectWithParensClauseParser settings a,
            NoParensSelectWithParens <$> SelectNoParens.unparenthesizedSelectNoParensParser settings
          ]

instance Qc.Arbitrary SelectWithParens where
  shrink = fmap canonicalize . Qc.genericShrink
  arbitrary =
    canonicalize
      <$> Qc.frequency
        [ (3, NoParensSelectWithParens <$> Gens.downscale Qc.arbitrary),
          (1, WithParensSelectWithParens <$> Gens.downscale Qc.arbitrary)
        ]

-- |
-- Collapses the non-canonical @NoParensSelectWithParens@ shape described
-- above to the @WithParensSelectWithParens@ shape the parser actually
-- produces for it. Both 'arbitrary' and 'shrink' can otherwise construct
-- the non-canonical shape (shrinking the inner 'SelectNoParens' toward
-- @Nothing@s is exactly how it arises), which renders fine but parses back
-- to a different, canonical value and so breaks the roundtrip property.
canonicalize :: SelectWithParens -> SelectWithParens
canonicalize = \case
  NoParensSelectWithParens a
    | Just c <- SelectNoParens.refineToSelectWithParens a -> WithParensSelectWithParens c
  other -> other

-- |
-- If a 'SelectWithParens' is the @WithParensSelectWithParens@ wrapping of
-- another one, returns the wrapped value. Exposed for modules that can
-- only see 'SelectWithParens' via its @hs-boot@ (which keeps it abstract
-- to break an import cycle) — see "PostgresqlSyntax.Ast.InExpr", which
-- needs it to canonicalize a @select_with_parens@\/@expr_list@ ambiguity
-- analogous to the one described above.
refineToSelectWithParens :: SelectWithParens -> Maybe SelectWithParens
refineToSelectWithParens = \case
  WithParensSelectWithParens a -> Just a
  _ -> Nothing

-- |
-- Smart constructor for the @WithParensSelectWithParens@ shape, for modules
-- that can only see 'SelectWithParens' via its @hs-boot@ (which keeps it
-- abstract to break an import cycle) — see "PostgresqlSyntax.Ast.CExpr",
-- which needs it to canonicalize a @'(' a_expr ')'@\/@select_with_parens@
-- ambiguity analogous to the one described above.
withParensSelectWithParens :: SelectWithParens -> SelectWithParens
withParensSelectWithParens = WithParensSelectWithParens