packages feed

swarm-0.4: src/Swarm/Language/LSP/VarUsage.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
module Swarm.Language.LSP.VarUsage where

import Control.Monad (guard)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map (Map)
import Data.Map qualified as M
import Data.Set (Set)
import Data.Set qualified as S
import Data.Text (Text)
import Data.Text qualified as T
import Language.LSP.Types qualified as J
import Swarm.Language.Parse qualified as P
import Swarm.Language.Syntax
import Swarm.Util qualified as U

data BindingType
  = Lambda
  | Let
  | Bind
  deriving (Eq, Show)

data VarUsage = VarUsage LocVar BindingType

type BindingSites = Map Var (NonEmpty SrcLoc)

data Usage = Usage
  { usages :: Set LocVar
  -- ^ Variable references
  , problems :: [VarUsage]
  -- ^ Variable declarations without any references
  }

instance Semigroup Usage where
  Usage y1 z1 <> Usage y2 z2 =
    Usage
      (y1 <> y2)
      (z1 <> z2)

instance Monoid Usage where
  mempty = Usage mempty mempty

toErrPos :: Text -> VarUsage -> Maybe (J.Range, Text)
toErrPos code (VarUsage (LV loc v) scope) = do
  -- A leading underscore will suppress the unused variable warning
  guard $ not $ "_" `T.isPrefixOf` v
  rangePair <- case loc of
    SrcLoc s e -> Just (s, e)
    _ -> Nothing
  let (start, end) = P.getLocRange code rangePair
      ((startLine, startCol), (endLine, endCol)) = (minusOne start, minusOne end)
      range =
        J.Range
          (J.Position (fromIntegral startLine) (fromIntegral startCol))
          (J.Position (fromIntegral endLine) (fromIntegral endCol))
  return (range, txt)
 where
  txt =
    T.unwords
      [ "Unused variable"
      , U.quote v
      , "in"
      , T.pack $ show scope
      , "expression"
      ]
  minusOne (x, y) = (x - 1, y - 1)

-- | Descends the syntax tree rooted at a variable declaration,
-- accumulating variable references.
-- Generates a "problem" if an associated variable reference
-- is not encountered in the subtree for this declaration.
checkOccurrences ::
  BindingSites ->
  LocVar ->
  BindingType ->
  [Syntax] ->
  Usage
checkOccurrences bindings lv@(LV loc v) declType childSyntaxes =
  Usage childUsages $ missing <> deeperMissing
 where
  deeperBindings = M.insertWith (<>) v (pure loc) bindings
  Usage childUsages deeperMissing = mconcat $ map (getUsage deeperBindings) childSyntaxes
  missing = [VarUsage lv declType | lv `S.notMember` childUsages]

-- | Build up the bindings map as a function argument as
-- we descend into the syntax tree.
-- Aggregates unused bindings as we return from each layer.
getUsage ::
  BindingSites ->
  Syntax ->
  Usage
getUsage bindings (Syntax _pos t) = case t of
  TVar v -> Usage myUsages mempty
   where
    myUsages = case M.lookup v bindings of
      Nothing -> mempty
      Just (loc :| _) -> S.singleton $ LV loc v
  SLam v _ s -> checkOccurrences bindings v Lambda [s]
  SApp s1 s2 -> getUsage bindings s1 <> getUsage bindings s2
  SLet _ v _ s1 s2 -> getUsage bindings s1 <> checkOccurrences bindings v Let [s2]
  SPair s1 s2 -> getUsage bindings s1 <> getUsage bindings s2
  SDef _ _v _ s -> getUsage bindings s
  SBind maybeVar s1 s2 -> case maybeVar of
    Just v -> checkOccurrences bindings v Bind [s1, s2]
    Nothing -> getUsage bindings s1 <> getUsage bindings s2
  SDelay _ s -> getUsage bindings s
  _ -> mempty