packages feed

glean-0.1.0.0: glean/hs/Glean/Schema/Resolve.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# LANGUAGE DeriveGeneric #-}

module Glean.Schema.Resolve
  ( parseAndResolveSchema
  , parseAndResolveSchemaCached
  , SchemaParserCache
  , resolveType
  , resolveSchema
  , runResolve
  , resolveQuery
  , resolveSchemaRefs
  ) where

import Control.Monad
import Control.Monad.Reader
import Control.Monad.Except
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import Data.Char
import Data.Graph
import Data.Foldable
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet as HashSet
import Data.HashSet (HashSet)
import Data.List
import Data.Maybe
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as Text
import Compat.Prettyprinter hiding (group)
import TextShow

import Glean.Angle.Hash
import Glean.Angle.Parser
import Glean.Angle.Types
import Glean.Schema.Types
import Glean.Schema.Util
import Glean.Schema.Evolve (validateResolvedEvolutions)

type SchemaParserCache = HashMap Hash SourceSchemas

-- ---------------------------------------------------------------------------
-- High-level schema parsing / resolution APIs

--
-- | Useful packaging of 'parseSchema' and 'resolveSchema'. Note that
-- parsing and resolution of a schema is a pure function.
--
parseAndResolveSchema
  :: ByteString
  -> Either String (SourceSchemas, ResolvedSchemas)
parseAndResolveSchema str =
  case parseSchema str of
    Left str -> Left str
    Right ss -> case resolveSchema ss of
      Left txt -> Left (Text.unpack txt)
      Right r -> Right (ss, r)

--
-- | Like parseAndResolveSchema but uses a cache to avoid repeatedly
-- parsing the same schema fragments.
--
-- We split the input string at "schema" declarations and hash the
-- content of each one to use as the cache key. There are a couple of
-- caveats here:
--
--  * we lose the benefit of the FILE annotations that tell us the
--    original source file name for error messages. Therefore don't
--    use this parsing method when you want error messages; use
--    the uncached parseAndResolveSchema instead.
--
--  * "schema" should appear in column 0. But if it doesn't, the worst
--    that can happen is we miss out on some caching. Since "schema" is
--    a keyword, it can't appear inside a schema or anything silly like
--    that.
--
parseAndResolveSchemaCached
  :: SchemaParserCache
  -> ByteString
  -> Either String (SourceSchemas, ResolvedSchemas, SchemaParserCache)
parseAndResolveSchemaCached cache str = do
  let
    (ver, rest) = stripAngleVersion str

    cutSchemas b =
      (h <> "\n") : -- put the "\n" back on the end
        if ByteString.null t
          then []
          else cutSchemas (ByteString.drop 1 t) -- drop the "\n"
      where (h,t) = ByteString.breakSubstring "\nschema" b

    schemaFragments =
      [ (hashByteString frag, frag)
      | frag <- cutSchemas rest ]

  parsed <- forM schemaFragments $ \(key, str) ->
    case HashMap.lookup key cache of
      Nothing -> case parseSchemaWithVersion ver str of
        Left err -> Left err
        Right ss -> return (key, ss)
      Just cached -> return (key, cached)

  let
    merged = SourceSchemas
      { srcAngleVersion = ver
      , srcSchemas = schemas
      , srcEvolves = evolves }
      where
        schemas = concatMap (srcSchemas . snd) parsed
        evolves = concatMap (srcEvolves . snd) parsed

    newCache = HashMap.union cache (HashMap.fromList parsed)

  case resolveSchema merged of
    Left txt -> Left (Text.unpack txt)
    Right r -> Right (merged, r, newCache)

--
-- | Turn 'SourceSchemas' into a 'ResolvedSchemas' by resolving all the
-- references and checking for validity.
--
resolveSchema :: SourceSchemas -> Either Text ResolvedSchemas
resolveSchema schemas = runExcept $ do
  checkAngleVersion (srcAngleVersion schemas)

  SourceSchemas{..} <- resolveSchemaRefs schemas

  let
    -- dependency analysis: we want to process schemas in dependency
    -- order, and detect cycles in evolves declarations.
    sccs = stronglyConnComp
      [ (schema, schemaName schema, out schema)
      | schema <- srcSchemas ]

    out s = schemaDependencies s <> evolvesOf (schemaName s)

    schemaDependencies SourceSchema{..} =
      schemaInherits ++ [ name | SourceImport name _ <- schemaDecls ]

    evolves = HashMap.fromListWith (++)
      [ (new, [old])
      | SourceEvolves _ new old <- srcEvolves
      ]

    evolvesOf name = HashMap.lookupDefault [] name evolves

    resolveSchemas env [] = return env
    resolveSchemas env (AcyclicSCC one : rest) = do
      let schemaEvolves = HashMap.lookupDefault [] (schemaName one) evolves
      resolved <- resolveOneSchema env srcAngleVersion
        schemaEvolves one
      resolveSchemas (HashMap.insert (schemaName one) resolved env) rest
    resolveSchemas _ (CyclicSCC some : _) = throwError $
      "cycle in schema definitions between: " <>
        Text.intercalate ", " (map (showRef . schemaName) some)

  -- Resolve all the references in each individual schema
  finalEnv <- resolveSchemas HashMap.empty sccs

  let
    resolved =
        [ schema
        | AcyclicSCC one <- sccs
        , Just schema <- [HashMap.lookup (schemaName one) finalEnv ] ]

    allSchemas =
      [ schema
      | schema@ResolvedSchema{..} <- HashMap.elems finalEnv
      , resolvedSchemaName == "all"
      ]

  when (srcAngleVersion >= AngleVersion 6) $
    liftEither $ validateResolvedEvolutions resolved

  return ResolvedSchemas
    { schemasHighestVersion =
        if null allSchemas
           then Nothing
           else Just (maximum $ map resolvedSchemaVersion allSchemas)
    , schemasResolved = resolved
    }


resolveSchemaRefs :: SourceSchemas -> Except Text SourceSchemas
resolveSchemaRefs SourceSchemas{..} = do
  let
     unqualMap
       | srcAngleVersion >= AngleVersion 10 =
         HashMap.fromListWith (<>)
           [ (SourceRef name Nothing, HashSet.singleton ref)
           | SourceSchema{..} <- srcSchemas
           , let ref@(SourceRef name _) = schemaName
           ]
       | otherwise = HashMap.empty

     refMap = unqualMap <> HashMap.fromList
       [ (schemaName, HashSet.singleton schemaName)
       | SourceSchema{..} <- srcSchemas
       ]

     schemaByName ref =
       case HashMap.lookup ref refMap of
         Nothing -> unknown
         Just s -> case HashSet.toList s of
           [one] -> return one
           names -> throwError $
             "ambiguous schema " <> showRef ref <>
             ", could be one of " <> Text.intercalate ", " (map showRef names)
       where
       unknown = throwError $ "unknown schema: " <> showRef ref

     schemaByNameWithNamespace parentSchema ref =
       let message e = e <> " within schema: " <> showRef parentSchema
       in withExcept message (schemaByName ref)

     resolveDecl parentSchema (SourceImport r s) =
      SourceImport <$> schemaByNameWithNamespace parentSchema r <*> pure s
     resolveDecl _ decl = return decl

     resolveEvolve (SourceEvolves l n o) =
       SourceEvolves l <$> schemaByName n <*> schemaByName o

     resolveSchema SourceSchema{..} = do
       inherits <- mapM (schemaByNameWithNamespace schemaName) schemaInherits
       decls <- mapM (resolveDecl schemaName) schemaDecls
       return SourceSchema
         { schemaName = schemaName
         , schemaInherits = inherits
         , schemaDecls = decls
         , schemaSrcSpan = schemaSrcSpan
         }

  schemas <- traverse resolveSchema srcSchemas
  evolves <- traverse resolveEvolve srcEvolves

  return SourceSchemas
    { srcAngleVersion = srcAngleVersion
    , srcSchemas = schemas
    , srcEvolves = evolves
    }

type Environment = HashMap SourceRef ResolvedSchemaRef

resolveOneSchema
  :: Environment
  -> AngleVersion
  -> [SourceRef]
  -> SourceSchema
  -> Except Text ResolvedSchemaRef

resolveOneSchema env angleVersion evolves SourceSchema{..} =
  let inSchema e = throwError $ "In " <> showRef schemaName <> ":\n  " <> e in
  flip catchError inSchema $ do
  let
    SourceRef namespace maybeVer = schemaName

    schemaByName name = case HashMap.lookup name env of
      Nothing -> throwError $ "unknown schema: " <> showRef name
      Just one -> return one

  checkNameSpace namespace

  -- Version of this schema
  version <- case maybeVer of
    Nothing -> throwError $ "missing version: " <> showRef schemaName
    Just v -> return v

  -- All the schemas we're inheriting from
  inherits <- traverse schemaByName schemaInherits

  -- All the schemas we imported
  imports <- traverse schemaByName [ name | SourceImport name _ <- schemaDecls ]

  let
    qualify :: Name -> Name
    qualify x = namespace <> "." <> x

    qualifyNameEnv :: NameEnv t -> NameEnv t
    qualifyNameEnv env = HashMap.fromList
      [ (SourceRef (qualify name) ver, target)
      | (SourceRef name ver, target) <- HashMap.toList env ]

    localPreds =
      [ let
          SourceRef name explicitVersion = predicateDefRef p
          thisVersion = fromMaybe version explicitVersion
          qname = qualify name
        in
          (name, PredicateRef qname thisVersion, p)
      | SourcePredicate p <- schemaDecls
      ]

    localTypes =
      [ let
          SourceRef name explicitVersion = typeDefRef p
          thisVersion = fromMaybe version explicitVersion
          qname = qualify name
        in
          (name, TypeRef qname thisVersion, p)
      | SourceType p <- schemaDecls
      ]

  -- Check for multiple definitions of the same name/version.
  -- Multiple definitions of the same name is OK: an unqualified
  -- reference will be rejected as ambiguous, but can be resolved by
  -- using an explicit version.
  let
    numRefs = HashMap.fromListWith (+) $
      [ ((predicateRef_name, predicateRef_version), 1::Int)
      | (_, PredicateRef{..}, _) <- localPreds ] ++
      [ ((typeRef_name, typeRef_version), 1)
      | (_, TypeRef{..}, _) <- localTypes]
  forM_ (HashMap.toList numRefs) $ \((name,ver), num) -> do
    when (num > 1) $ throwError $
      "multiple definitions for: " <> name <> "." <> showt ver

  -- Build the scope: a mapping from unversioned names to RefTarget
  let
    -- inherited definitions are in scope unqualified and qualified
    --   (but unqualified local names override unqualified inherited names)
    qualInheritedScope :: NameEnv RefResolved
    qualInheritedScope =
      unionNameEnvs $ map resolvedSchemaQualScope inherits

    unqualInheritedScope :: NameEnv RefResolved
    unqualInheritedScope =
      unionNameEnvs $ map resolvedSchemaUnqualScope inherits

    -- imported names are in scope qualified only
    importedScope :: NameEnv RefResolved
    importedScope = unionNameEnvs $ map resolvedSchemaQualScope imports

    unionNameEnvs :: [NameEnv RefResolved] -> NameEnv RefResolved
    unionNameEnvs = foldl' (HashMap.unionWith Set.union) HashMap.empty

    nameEntries name ver target =
      [ (SourceRef name Nothing, set),
        (SourceRef name (Just ver), set) ]
      where set = Set.singleton target

    -- local definitions are in scope unqualified and qualified
    unqualLocalScope :: NameEnv RefResolved
    unqualLocalScope = HashMap.fromListWith Set.union unqualLocalEntities

    qualLocalScope :: NameEnv RefResolved
    qualLocalScope = qualifyNameEnv unqualLocalScope

    unqualLocalEntities =
      -- P and P.1 for each local predicate
      [ entry
      | (name, r, _) <- localPreds
      , entry <- nameEntries name (predicateRef_version r) (RefPred r)
      ] ++
      -- T and T.1 for each local type
      [ entry
      | (name, r, _) <- localTypes
      , entry <- nameEntries name (typeRef_version r) (RefType r)
      ]

    scope =
      HashMap.union unqualLocalScope $
      HashMap.union unqualInheritedScope $
      HashMap.unionWith Set.union qualLocalScope $
      HashMap.unionWith Set.union qualInheritedScope
      importedScope

  -- Check for inheriting multiple versions of a predicate/types
  --
  -- As a special case, if the schema is called "all" then allow name
  -- clashes in the inheritance set. This is to facilitate our "all.N"
  -- convention for declaring the "top-level" schema which will
  -- inevitably have name clashes, but we don't care about what it
  -- exports.
  unless (namespace == "all") $
    forM_ (HashMap.toList unqualInheritedScope) $ \(ref, targets) -> do
      case Set.elems targets of
        (_:_:_) -> throwError $
          "inherited schemas give multiple definitions for: " <> showRef ref
        _ -> return ()

  -- resolve type definitions
  types <- forM localTypes $
    \(name, ref, TypeDef{..}) -> do
      checkName name
      type' <- runResolve angleVersion scope (resolveType typeDefType)
      return (name, TypeDef
        { typeDefRef = ref
        , typeDefType = type'
        , typeDefSrcSpan = typeDefSrcSpan })

  -- resolve predicate definitions
  predicates <- forM localPreds $
    \(name, ref, PredicateDef{..}) -> do
      checkName name
      runResolve angleVersion scope $ do
        key <- resolveType predicateDefKeyType
        value <- resolveType predicateDefValueType
        return (name, PredicateDef
          { predicateDefRef = ref
          , predicateDefKeyType = key
          , predicateDefValueType = value
          , predicateDefDeriving = NoDeriving
          , predicateDefSrcSpan = predicateDefSrcSpan })

  let
    -- scope of predicates that we can specify queries for. Namely
    -- locally-defined predicates and inherited predicates.
    predScope = resolveRef $
      HashMap.union unqualLocalScope $
      HashMap.union unqualInheritedScope $
      HashMap.unionWith Set.union qualLocalScope
      qualInheritedScope

  -- resolve queries
  localDeriving <-
    forM [ (derivingDefRef,derive)
      | SourceDeriving derive@DerivingDef{..} <- schemaDecls ] $
    \(ref, DerivingDef{..}) -> do
      ty <- lookupResultToExcept ref $ predScope ref
      ref <- case ty of
        RefPred ref -> return ref
        _ -> throwError $ showRef ref <> " is not a predicate"
      resolvedDerInfo <- runResolve angleVersion scope $
        resolveDeriving derivingDefDeriveInfo
      return (ref, DerivingDef ref resolvedDerInfo derivingDefSrcSpan)

  let
    localTypeNames = HashSet.fromList $
      map (typeRef_name . typeDefRef . snd) types

    localTypes = HashMap.fromList
      [ (typeDefRef def, def) | (_, def) <- types ]

    -- The types we re-export from this schema are all the types that
    -- are inherited but not shadowed by a locally-defined type.
    reExportedTypes =
      HashMap.fromList
        [ (typeDefRef def, def)
        | schema <- inherits
        , (ref, def) <- HashMap.toList $
            resolvedSchemaTypes schema <>
            resolvedSchemaReExportedTypes schema
        , not (typeRef_name ref `HashSet.member` localTypeNames)
        ]

    localPredicateNames = HashSet.fromList $
      [ predicateRef_name (predicateDefRef def) | (_, def) <- predicates ]

    localPredicates = HashMap.fromList
      [ (predicateDefRef def, def) | (_, def) <- predicates ]

    -- Similarly for predicates.
    reExportedPredicates =
      HashMap.fromList
        [ (predicateDefRef def, def)
        | schema <- inherits
        , (ref, def) <- HashMap.toList $
            resolvedSchemaPredicates schema <>
            resolvedSchemaReExportedPredicates schema
        , not (predicateRef_name ref `HashSet.member` localPredicateNames)
        ]

    exportedUnqualScope = HashMap.union unqualLocalScope unqualInheritedScope
      -- Note: local names override inherited names

    -- Qualified exported names. The "all" schema is special: we don't
    -- re-export things qualified with "all.", only with the original
    -- schema name.
    exportedQualScope
      | namespace == "all" = qualInheritedScope
      | otherwise =
         HashMap.union
           (qualifyNameEnv exportedUnqualScope)
           qualInheritedScope -- NB. local overrides inherited

  schemaEvolves <- traverse schemaByName evolves

  return ResolvedSchema
    { resolvedSchemaName = namespace
    , resolvedSchemaVersion = version
    , resolvedSchemaAngleVersion = angleVersion
    , resolvedSchemaTypes = localTypes
    , resolvedSchemaReExportedTypes = reExportedTypes
    , resolvedSchemaPredicates = localPredicates
    , resolvedSchemaReExportedPredicates = reExportedPredicates
    , resolvedSchemaUnqualScope = exportedUnqualScope
    , resolvedSchemaQualScope = exportedQualScope
    , resolvedSchemaDeriving = HashMap.fromList localDeriving
    , resolvedSchemaEvolves = Set.fromList (schemaRef <$> schemaEvolves)
    }

resolveType :: (ShowRef t, ShowRef p) =>
  SourceType -> Resolve p t (Type_ SrcSpan p t)
resolveType typ = go typ
  where
  go typ = case typ of
    ByteTy -> return ByteTy
    NatTy -> return NatTy
    StringTy -> return StringTy
    ArrayTy ty -> ArrayTy <$> go ty
    RecordTy fields -> do checkFields fields; RecordTy <$> mapM goField fields
    SumTy fields -> do checkFields fields; SumTy <$> mapM goField fields
    SetTy ty -> SetTy <$> go ty
    PredicateTy s ref -> goRef ref s
    NamedTy s ref -> goRef ref s -- shouldn't happen, but handle it anyway
    MaybeTy ty -> MaybeTy <$> go ty
    EnumeratedTy names -> lift $ do
      mapM_ checkName names
      return (EnumeratedTy names)
    BooleanTy -> return BooleanTy
    _ -> error "resolveType"

  goRef ref s = do
    scope <- getScope
    target <- lift $ lookupResultToExcept ref $
      resolveRef scope ref
    case target of
      RefType ref -> return (NamedTy s ref)
      RefPred ref -> return (PredicateTy s ref)

  checkFields fields = do
    sequence_
      [ throwError $ "duplicate field: " <> x
        | x:_:_ <- group $ sort $ map fieldDefName fields ]
    lift $ do
      mapM_ (checkName . fieldDefName) fields
      mapM_ (checkFieldName . fieldDefName) fields

  goField (FieldDef name ty) = FieldDef name <$> go ty


lookupResultToExcept
  :: (ShowRef t, ShowRef p)
  => SourceRef
  -> LookupResult (RefTarget p t)
  -> Except Text (RefTarget p t)
lookupResultToExcept ref res =
  either throwError return (lookupResultToEither ref res)

checkAngleVersion :: AngleVersion -> Except Text ()
checkAngleVersion v =
  unless (v >= latestSupportedAngleVersion) $
    let ver = Text.pack $ show $ pretty v in
    throwError $ "Angle version " <> ver <> " is not supported"

checkFieldName :: Name -> Except Text ()
checkFieldName n =
  when (isUpper (Text.head n)) $
    throwError $ "field names must begin with a lowercase letter: " <> n

checkName :: Name -> Except Text ()
checkName n = do
  when (n `HashSet.member` reservedWords) $
    throwError $ n <> " is a reserved word, it cannot be used"

checkNameSpace :: Name -> Except Text ()
checkNameSpace n = mapM_ checkName (splitDot n)

reservedWords :: HashSet Text
reservedWords = HashSet.fromList [
    -- Thrift reserved words, seems like a good idea to avoid these
    "binary", "bool", "byte", "const", "cpp_include",
    "double", "enum", "exception", "extends", "false", "float",
    "hash_map", "hash_set", "hs_include", "i16", "i32", "i64", "include",
    "list", "map", "namespace", "oneway", "optional", "required", "senum",
    "service", "set", "stream", "string", "struct", "throws", "true",
    "typedef", "union", "view", "void",

    -- keywords that the fbthrift compiler rejects (see
    -- thrift/compiler/parse/thrift.ll)
    "abstract",
    "and",
    "args",
    "as",
    "assert",
    "auto",
    "break",
    "case",
    "char",
    "class",
    "continue",
    "declare",
    "def",
    "default",
    "del",
    "do",
    "elif",
    "else",
    "elseif",
    "except",
    "exec",
    "extern",
    "finally",
    "for",
    "foreach",
    "function",
    "global",
    "goto",
    "if",
    "implements",
    "import",
    "in",
    "int",
    "inline",
    "instanceof",
    "interface",
    "is",
    "lambda",
    "long",
    "native",
    "new",
    "not",
    "or",
    "pass",
    "public",
    "print",
    "private",
    "protected",
    "raise",
    "register",
    "return",
    "short",
    "signed",
    "sizeof",
    "static",
    "switch",
    "synchronized",
    "template",
    "this",
    "throw",
    "transient",
    "try",
    "unsigned",
    "var",
    "virtual",
    "volatile",
    "while",
    "with",
    "yield",
    "Object",
    "Client",
    "IFace",
    "Processor",

    -- Other words to avoid, because they lead to Thrift-generated code that
    -- does not compile for one or more languages.
    "None", "operator", {- TODO: "Enum" -}

    -- Reserved for temporary predicates
    "_tmp_"
  ]


-- -----------------------------------------------------------------------------
-- Resolving queries

runResolve
  :: AngleVersion
  -> NameEnv (RefTarget p t)
  -> Resolve p t a
  -> Except Text a
runResolve ver scope act = runReaderT act (ver,scope)

type Resolve p t a =
  ReaderT (AngleVersion, NameEnv (RefTarget p t)) (Except Text) a

getScope :: Resolve p t (NameEnv (RefTarget p t))
getScope = asks snd

resolveDeriving
  :: (ShowRef t, ShowRef p)
  => SourceDerivingInfo' SrcSpan
  -> Resolve p t (DerivingInfo (Query_ p t))
resolveDeriving NoDeriving = return NoDeriving
resolveDeriving (Derive when query) = Derive when <$> resolveQuery query

resolveQuery
  :: (ShowRef t, ShowRef p)
  => SourceQuery
  -> Resolve p t (Query_ p t)
resolveQuery (SourceQuery head stmts ord) =
  SourceQuery
    <$> mapM resolvePat head
    <*> mapM resolveStatement stmts
    <*> pure ord

resolvePat
  :: (ShowRef t, ShowRef p)
  => SourcePat
  -> Resolve p t (SourcePat_ SrcSpan SrcSpan p t)
resolvePat pat = case pat of
  Nat s i -> return (Nat s i)
  String s t -> return (String s t)
  StringPrefix s t -> return (StringPrefix s t)
  ByteArray s b -> return (ByteArray s b)
  Array s pats -> Array s <$> mapM resolvePat pats
  ArrayPrefix s pats -> ArrayPrefix s <$> mapM resolvePat pats
  Tuple s pats -> Tuple s <$> mapM resolvePat pats
  All s qs -> All s <$> resolvePat qs
  Elements s pat -> Elements s <$> resolvePat pat
  Wildcard s -> return (Wildcard s)
  TypeSignature s pat ty ->
    TypeSignature s
      <$> resolvePat pat
      <*> resolveType ty
  Variable s n -> return (Variable s n)
  FactId s Nothing id -> return (FactId s Nothing id)
  FactId s (Just pred) id -> do
    res <- resolveTypeOrPred s pred
    case res of
      RefPred ref ->
        return (TypeSignature s (FactId s Nothing id) (PredicateTy s ref))
      _other -> prettyErrorIn pat $ "not a predicate: " <> pretty pred
  OrPattern s l r -> OrPattern s <$> resolvePat l <*> resolvePat r
  Negation s pat -> Negation s <$> resolvePat pat
  Never s -> return (Never s)
  IfPattern s cond then_ else_ ->
    IfPattern s
      <$> resolvePat cond
      <*> resolvePat then_
      <*> resolvePat else_
  ElementsOfArray s pat -> ElementsOfArray s <$> resolvePat pat
  KeyValue s k v -> KeyValue s <$> resolvePat k <*> resolvePat v
  NestedQuery s q -> NestedQuery s <$> resolveQuery q
  Struct s fields -> Struct s <$> mapM resolveField fields
    where resolveField (Field n pat) = Field n <$> resolvePat pat
  App s (Variable svar txt) args
    | Just primOp <- HashMap.lookup txt primitives -> do
      Prim s primOp <$> mapM resolvePat args
    | otherwise ->
    case args of
      [arg] -> do
        arg' <- resolvePat arg
        let (range, txt') = if
              | Just rem <- Text.stripSuffix "#new" txt -> (SeekOnStacked, rem)
              | Just rem <- Text.stripSuffix "#old" txt -> (SeekOnBase, rem)
              | otherwise -> (SeekOnAllFacts, txt)
        res <- resolveTypeOrPred svar txt'
        case res of
          RefPred ref -> return (Clause s svar ref arg' range)
          RefType ref
            | SeekOnAllFacts <- range ->
              return (TypeSignature s arg' (NamedTy svar ref))
            | otherwise ->
              prettyErrorIn pat "cannot use #new or #old with types"
            -- The syntax "T pat" for "pat : T" is something we might
            -- consider deprecating later. For now it's just desugared
            -- here.
      _ -> prettyErrorIn pat "unexpected extra argument(s)"
  App s (StringPrefix a b) [pat] -> do
    pat' <- resolvePat pat
    return (App s (StringPrefix a b) [pat'])
  App _ f _ ->
    prettyErrorIn pat $ "invalid pattern: cannot apply " <>
    describe f <> " to arguments"
  FieldSelect s pat field q ->
    (\p -> FieldSelect s p field q)
      <$> resolvePat pat
  Deref s pat -> Deref s <$> resolvePat pat
  Enum s n -> return (Enum s n)
  Clause{} -> internal
  Prim{} -> internal
  where
  internal = throwError $ "internal: unexpected: " <>
    Text.pack (show (pretty pat))

resolveTypeOrPred
  :: (ShowRef p, ShowRef t)
  => SrcSpan
  -> Name
  -> Resolve p t (RefTarget p t)
resolveTypeOrPred span txt = do
  scope <- getScope
  let ref = parseRef txt
  case lookupResultToEither ref $ resolveRef scope ref of
    Left s -> prettyErrorAt span (pretty s)
    Right r -> return r

prettyError :: Doc ann -> Resolve p t a
prettyError = throwError . Text.pack . show

prettyErrorIn :: IsSrcSpan s => SourcePat' s st -> Doc ann -> Resolve p t a
prettyErrorIn pat doc = prettyErrorAt (sourcePatSpan pat) doc

prettyErrorAt :: IsSrcSpan span => span -> Doc ann -> Resolve p t a
prettyErrorAt span doc = prettyError $ vcat
  [ pretty span
  , doc
  ]

resolveStatement
  :: (ShowRef t, ShowRef p)
  => SourceStatement
  -> Resolve p t (Statement_ p t)
resolveStatement (SourceStatement l r) =
  SourceStatement <$> resolvePat l <*> resolvePat r

primitives :: HashMap Text PrimOp
primitives = HashMap.fromList
  [ ("prim.toLower", PrimOpToLower)
  , ("prim.length", PrimOpLength)
  , ("prim.relToAbsByteSpans", PrimOpRelToAbsByteSpans)
  , ("prim.unpackByteSpans", PrimOpUnpackByteSpans)
  , ("prim.zip", PrimOpZip)
  , ("prim.concat", PrimOpConcat)
  , ("prim.reverse", PrimOpReverse)
  , ("prim.size", PrimOpSize)
  , ("prim.gtNat", PrimOpGtNat)
  , ("prim.geNat", PrimOpGeNat)
  , ("prim.ltNat", PrimOpLtNat)
  , ("prim.leNat", PrimOpLeNat)
  , ("prim.neNat", PrimOpNeNat)
  , ("prim.addNat", PrimOpAddNat)
  , ("prim.neExpr", PrimOpNeExpr)
  ]