1
1
mirror of https://github.com/github/semantic.git synced 2024-11-28 10:15:55 +03:00

Merge pull request #1828 from github/builtins

Builtins
This commit is contained in:
Josh Vera 2018-05-11 11:27:09 -04:00 committed by GitHub
commit 2b6dea75ef
17 changed files with 229 additions and 147 deletions

View File

@ -1,2 +1,3 @@
def print(x):
__semantic_print(x)
return x

View File

@ -7,3 +7,7 @@ class Object
return "<object>"
end
end
def puts(obj)
__semantic_print(obj)
end

View File

@ -8,6 +8,7 @@ module Control.Abstract.Context
, Span
, currentSpan
, withCurrentSpan
, withCurrentCallStack
) where
import Control.Effect
@ -15,6 +16,7 @@ import Control.Monad.Effect.Reader
import Data.Abstract.Module
import Data.Abstract.Package
import Data.Span
import GHC.Stack
import Prologue
-- | Get the currently evaluating 'ModuleInfo'.
@ -40,3 +42,14 @@ currentSpan = raise ask
-- | Run an action with a locally-replaced 'Span'.
withCurrentSpan :: (Effectful m, Member (Reader Span) effects) => Span -> m effects a -> m effects a
withCurrentSpan = raiseHandler . local . const
-- | Run an action with locally-replaced 'ModuleInfo' & 'Span' derived from the passed 'SrcLoc'.
withCurrentSrcLoc :: (Effectful m, Members '[Reader ModuleInfo, Reader Span] effects) => SrcLoc -> m effects a -> m effects a
withCurrentSrcLoc loc = withCurrentModule (moduleInfoFromSrcLoc loc) . withCurrentSpan (spanFromSrcLoc loc)
-- | Run ana ction with locally replaced 'ModuleInfo' & 'Span' derived from the Haskell call stack.
--
-- This is suitable for contextualizing builtins & other functionality intended for use from client code but defined in Haskell source.
withCurrentCallStack :: (Effectful m, Members '[Reader ModuleInfo, Reader Span] effects) => CallStack -> m effects a -> m effects a
withCurrentCallStack = maybe id (withCurrentSrcLoc . snd) . listToMaybe . getCallStack

View File

@ -9,12 +9,12 @@ module Control.Abstract.Goto
) where
import Control.Abstract.Evaluator
import Control.Monad.Effect (Eff, relayState)
import Control.Monad.Effect (Eff)
import qualified Data.IntMap as IntMap
import Prelude hiding (fail)
import Prologue
type GotoTable effects value = IntMap.IntMap (Eff effects value)
type GotoTable inner value = IntMap.IntMap (Eff (Goto inner value ': inner) value)
-- | The type of labels.
-- TODO: This should be rolled into 'Name' and tracked in the environment, both so that we can abstract over labels like any other location, and so that we can garbage collect unreachable labels.
@ -27,7 +27,7 @@ type Label = Int
label :: Evaluator location value (Goto effects value ': effects) value -> Evaluator location value (Goto effects value ': effects) Label
label = send . Label . lower
-- | “Jump” to a previously-allocated 'Label' (retrieving the @term@ at which it points, which can then be evaluated in e.g. a 'MonadAnalysis' instance).
-- | “Jump” to a previously-allocated 'Label' (retrieving the @term@ at which it points, which can then be evaluated.
goto :: Label -> Evaluator location value (Goto effects value ': effects) (Evaluator location value (Goto effects value ': effects) value)
goto = fmap raise . send . Goto
@ -45,7 +45,34 @@ data Goto effects value return where
Label :: Eff (Goto effects value ': effects) value -> Goto effects value Label
Goto :: Label -> Goto effects value (Eff (Goto effects value ': effects) value)
runGoto :: Member Fail effects => GotoTable (Goto effects value ': effects) value -> Evaluator location value (Goto effects value ': effects) a -> Evaluator location value effects (a, GotoTable (Goto effects value ': effects) value)
runGoto initial = raiseHandler (relayState (IntMap.size initial, initial) (\ (_, table) a -> pure (a, table)) (\ (supremum, table) goto yield -> case goto of
Label action -> yield (succ supremum, IntMap.insert supremum action table) supremum
Goto label -> maybe (fail ("unknown label: " <> show label)) (yield (supremum, table)) (IntMap.lookup label table)))
-- | Run a 'Goto' effect in terms of a 'State' effect holding a 'GotoTable', accessed via wrap/unwrap functions.
--
-- The wrap/unwrap functions are necessary in order for ghc to be able to typecheck the table, since it necessarily contains references to its own effect list. Since @GotoTable (… ': State (GotoTable … value) ': …) value@ cant be written, and a recursive type equality constraint wont typecheck, callers will need to employ a @newtype@ to break the self-reference. The effect list of the table the @newtype@ contains will include all of the effects between the 'Goto' effect and the 'State' effect (including the 'State' but not the 'Goto'). E.g. if the 'State' is the next effect, a valid wrapper would be∷
--
-- @
-- newtype Gotos effects value = Gotos { getGotos :: GotoTable (State (Gotos effects value) ': effects) value }
-- @
--
-- Callers can then evaluate the high-level 'Goto' effect by passing @Gotos@ and @getGotos@ to 'runGoto'.
runGoto :: Members '[ Fail
, Fresh
, State table
] effects
=> (GotoTable effects value -> table)
-> (table -> GotoTable effects value)
-> Evaluator location value (Goto effects value ': effects) a
-> Evaluator location value effects a
runGoto from to = runEffect (\ goto yield -> do
table <- to <$> getTable
case goto of
Label action -> do
supremum <- raise fresh
putTable (from (IntMap.insert supremum action table))
yield supremum
Goto label -> maybe (raise (fail ("unknown label: " <> show label))) yield (IntMap.lookup label table))
getTable :: Member (State table) effects => Evaluator location value effects table
getTable = raise get
putTable :: Member (State table) effects => table -> Evaluator location value effects ()
putTable = raise . put

View File

@ -37,44 +37,44 @@ import qualified Data.List.NonEmpty as NonEmpty
-- | A LIFO stack of maps of names to addresses, representing a lexically-scoped evaluation environment.
-- All behaviors can be assumed to be frontmost-biased: looking up "a" will check the most specific
-- scope for "a", then the next, and so on.
newtype Environment l a = Environment { unEnvironment :: NonEmpty (Map.Map Name (Address l a)) }
newtype Environment location value = Environment { unEnvironment :: NonEmpty (Map.Map Name (Address location value)) }
deriving (Eq, Foldable, Functor, Generic1, Ord, Show, Traversable)
instance Eq l => Eq1 (Environment l) where liftEq = genericLiftEq
instance Ord l => Ord1 (Environment l) where liftCompare = genericLiftCompare
instance Show l => Show1 (Environment l) where liftShowsPrec = genericLiftShowsPrec
instance Eq location => Eq1 (Environment location) where liftEq = genericLiftEq
instance Ord location => Ord1 (Environment location) where liftCompare = genericLiftCompare
instance Show location => Show1 (Environment location) where liftShowsPrec = genericLiftShowsPrec
-- | The provided list will be put into an Environment with one member, so fromList is total
-- (despite NonEmpty's instance being partial). Don't pass in multiple Addresses for the
-- same Name or you violate the axiom that toList . fromList == id.
instance IsList (Environment l a) where
type Item (Environment l a) = (Name, Address l a)
instance IsList (Environment location value) where
type Item (Environment location value) = (Name, Address location value)
fromList xs = Environment (Map.fromList xs :| [])
toList (Environment (x :| _)) = Map.toList x
mergeEnvs :: Environment l a -> Environment l a -> Environment l a
mergeEnvs :: Environment location value -> Environment location value -> Environment location value
mergeEnvs (Environment (a :| as)) (Environment (b :| bs)) =
Environment ((<>) a b :| alignWith (mergeThese (<>)) as bs)
emptyEnv :: Environment l a
emptyEnv :: Environment location value
emptyEnv = Environment (lowerBound :| [])
-- | Make and enter a new empty scope in the given environment.
push :: Environment l a -> Environment l a
push :: Environment location value -> Environment location value
push (Environment (a :| as)) = Environment (mempty :| a : as)
-- | Remove the frontmost scope.
pop :: Environment l a -> Environment l a
pop :: Environment location value -> Environment location value
pop (Environment (_ :| [])) = emptyEnv
pop (Environment (_ :| a : as)) = Environment (a :| as)
-- | Drop all scopes save for the frontmost one.
head :: Environment l a -> Environment l a
head :: Environment location value -> Environment location value
head (Environment (a :| _)) = Environment (a :| [])
-- | Take the union of two environments. When duplicate keys are found in the
-- name to address map, the second definition wins.
mergeNewer :: Environment l a -> Environment l a -> Environment l a
mergeNewer :: Environment location value -> Environment location value -> Environment location value
mergeNewer (Environment a) (Environment b) =
Environment (NonEmpty.fromList . reverse $ alignWith (mergeThese combine) (reverse as) (reverse bs))
where
@ -86,45 +86,45 @@ mergeNewer (Environment a) (Environment b) =
--
-- >>> pairs shadowed
-- [(Name {unName = "foo"},Address {unAddress = Precise {unPrecise = 1}})]
pairs :: Environment l a -> [(Name, Address l a)]
pairs :: Environment location value -> [(Name, Address location value)]
pairs = Map.toList . fold . unEnvironment
unpairs :: [(Name, Address l a)] -> Environment l a
unpairs :: [(Name, Address location value)] -> Environment location value
unpairs = fromList
-- | Lookup a 'Name' in the environment.
--
-- >>> lookup (name "foo") shadowed
-- Just (Address {unAddress = Precise {unPrecise = 1}})
lookup :: Name -> Environment l a -> Maybe (Address l a)
lookup :: Name -> Environment location value -> Maybe (Address location value)
lookup k = foldMapA (Map.lookup k) . unEnvironment
-- | Insert a 'Name' in the environment.
insert :: Name -> Address l a -> Environment l a -> Environment l a
insert :: Name -> Address location value -> Environment location value -> Environment location value
insert name value (Environment (a :| as)) = Environment (Map.insert name value a :| as)
-- | Remove a 'Name' from the environment.
--
-- >>> delete (name "foo") shadowed
-- Environment {unEnvironment = fromList [] :| []}
delete :: Name -> Environment l a -> Environment l a
delete :: Name -> Environment location value -> Environment location value
delete name = trim . Environment . fmap (Map.delete name) . unEnvironment
trim :: Environment l a -> Environment l a
trim :: Environment location value -> Environment location value
trim (Environment (a :| as)) = Environment (a :| filtered)
where filtered = filter (not . Map.null) as
bind :: Foldable t => t Name -> Environment l a -> Environment l a
bind :: Foldable t => t Name -> Environment location value -> Environment location value
bind names env = fromList (mapMaybe lookupName (Prologue.toList names))
where
lookupName name = (,) name <$> lookup name env
-- | Get all bound 'Name's in an environment.
names :: Environment l a -> [Name]
names :: Environment location value -> [Name]
names = fmap fst . pairs
-- | Lookup and alias name-value bindings from an environment.
overwrite :: [(Name, Name)] -> Environment l a -> Environment l a
overwrite :: [(Name, Name)] -> Environment location value -> Environment location value
overwrite pairs env = fromList $ mapMaybe lookupAndAlias pairs
where
lookupAndAlias (oldName, newName) = (,) newName <$> lookup oldName env
@ -132,10 +132,10 @@ overwrite pairs env = fromList $ mapMaybe lookupAndAlias pairs
-- | Retrieve the 'Live' set of addresses to which the given free variable names are bound.
--
-- Unbound names are silently dropped.
roots :: (Ord l, Foldable t) => Environment l a -> t Name -> Live l a
roots :: (Ord location, Foldable t) => Environment location value -> t Name -> Live location value
roots env = foldMap (maybe mempty liveSingleton . flip lookup env)
addresses :: Ord l => Environment l a -> Live l a
addresses :: Ord location => Environment location value -> Live location value
addresses = Live . fromList . fmap snd . pairs

View File

@ -1,4 +1,4 @@
{-# LANGUAGE ConstraintKinds, DefaultSignatures, GADTs, RankNTypes, ScopedTypeVariables, TypeOperators, UndecidableInstances #-}
{-# LANGUAGE ConstraintKinds, DefaultSignatures, GADTs, GeneralizedNewtypeDeriving, RankNTypes, ScopedTypeVariables, TypeOperators, UndecidableInstances #-}
module Data.Abstract.Evaluatable
( module X
, Evaluatable(..)
@ -12,6 +12,7 @@ module Data.Abstract.Evaluatable
, evaluatePackageWith
, throwEvalError
, traceResolve
, builtin
, isolate
, Modules
) where
@ -29,6 +30,7 @@ import Data.Abstract.FreeVariables as X
import Data.Abstract.Module
import Data.Abstract.ModuleTable as ModuleTable
import Data.Abstract.Package as Package
import Data.ByteString.Char8 (pack, unpack)
import Data.Scientific (Scientific)
import Data.Semigroup.App
import Data.Semigroup.Foldable
@ -166,11 +168,31 @@ traceResolve :: (Show a, Show b, Member Trace effects) => a -> b -> Evaluator lo
traceResolve name path = traceE ("resolved " <> show name <> " -> " <> show path)
builtin :: ( Addressable location effects
, HasCallStack
, Members '[ Reader (Environment location value)
, Reader ModuleInfo
, Reader Span
, State (Environment location value)
, State (Heap location value)
] effects
, Reducer value (Cell location value)
)
=> String
-> Evaluator location value effects value
-> Evaluator location value effects ()
builtin n def = withCurrentCallStack callStack $ do
let name = X.name ("__semantic_" <> pack n)
addr <- alloc name
modifyEnv (X.insert name addr)
def >>= assign addr
-- | Evaluate a given package.
evaluatePackageWith :: forall location term value inner inner' outer
. ( Evaluatable (Base term)
, EvaluatableConstraints location term value inner
, Members '[ Fail
, Fresh
, Reader (Environment location value)
, Resumable (LoadError location value)
, State (Environment location value)
@ -180,7 +202,7 @@ evaluatePackageWith :: forall location term value inner inner' outer
] outer
, Recursive term
, inner ~ (Goto inner' value ': inner')
, inner' ~ (LoopControl value ': Return value ': Reader ModuleInfo ': Modules location value ': Reader Span ': Reader PackageInfo ': outer)
, inner' ~ (LoopControl value ': Return value ': Reader ModuleInfo ': Modules location value ': State (Gotos location value (Reader Span ': Reader PackageInfo ': outer)) ': Reader Span ': Reader PackageInfo ': outer)
)
=> (SubtermAlgebra Module term (Evaluator location value inner value) -> SubtermAlgebra Module term (Evaluator location value inner value))
-> (SubtermAlgebra (Base term) term (Evaluator location value inner value) -> SubtermAlgebra (Base term) term (Evaluator location value inner value))
@ -189,6 +211,8 @@ evaluatePackageWith :: forall location term value inner inner' outer
evaluatePackageWith analyzeModule analyzeTerm package
= runReader (packageInfo package)
. runReader lowerBound
. fmap fst
. runState (lowerBound :: Gotos location value (Reader Span ': Reader PackageInfo ': outer))
. runReader (packageModules (packageBody package))
. runModules evalModule
. withPrelude (packagePrelude (packageBody package))
@ -203,16 +227,18 @@ evaluatePackageWith analyzeModule analyzeTerm package
= runReader info
. runReturn
. runLoopControl
. fmap fst
. runGoto lowerBound
. runGoto Gotos getGotos
evaluateEntryPoint :: ModulePath -> Maybe Name -> Evaluator location value (Modules location value ': Reader Span ': Reader PackageInfo ': outer) value
evaluateEntryPoint :: ModulePath -> Maybe Name -> Evaluator location value (Modules location value ': State (Gotos location value (Reader Span ': Reader PackageInfo ': outer)) ': Reader Span ': Reader PackageInfo ': outer) value
evaluateEntryPoint m sym = runInModule (ModuleInfo m) $ do
v <- maybe unit (pure . snd) <$> require m
maybe v ((`call` []) <=< variable) sym
withPrelude Nothing a = a
withPrelude (Just prelude) a = do
_ <- runInModule moduleInfoFromCallStack $ do
builtin "print" (closure ["s"] lowerBound (variable "s" >>= asString >>= traceE . unpack >> unit))
unit
preludeEnv <- fst <$> evalModule prelude
withDefaultEnvironment preludeEnv a
@ -224,6 +250,9 @@ evaluatePackageWith analyzeModule analyzeTerm package
| otherwise = Exports.toEnvironment ports `mergeEnvs` overwrite (Exports.aliases ports) env
pairValueWithEnv action = flip (,) <$> action <*> (filterEnv <$> getExports <*> getEnv)
newtype Gotos location value outer = Gotos { getGotos :: GotoTable (LoopControl value ': Return value ': Reader ModuleInfo ': Modules location value ': State (Gotos location value outer) ': outer) value }
deriving (Lower)
-- | Isolate the given action with an empty global environment and exports.
isolate :: Members '[State (Environment location value), State (Exports location value)] effects => Evaluator location value effects a -> Evaluator location value effects a

View File

@ -1,19 +1,17 @@
module Data.Abstract.Module
( Module(..)
, ModuleInfo(..)
, ModulePath
, moduleForBlob
, ModulePath
, ModuleInfo(..)
, moduleInfoFromSrcLoc
, moduleInfoFromCallStack
) where
import Data.Blob
import GHC.Stack
import Prologue
import System.FilePath.Posix
type ModulePath = FilePath
newtype ModuleInfo = ModuleInfo { modulePath :: ModulePath }
deriving (Eq, Ord, Show)
data Module term = Module { moduleInfo :: ModuleInfo, moduleBody :: term }
deriving (Eq, Foldable, Functor, Ord, Traversable)
@ -29,3 +27,16 @@ moduleForBlob :: Maybe FilePath -- ^ The root directory relative to which the mo
moduleForBlob rootDir Blob{..} = Module info
where root = fromMaybe (takeDirectory blobPath) rootDir
info = ModuleInfo (makeRelative root blobPath)
type ModulePath = FilePath
newtype ModuleInfo = ModuleInfo { modulePath :: ModulePath }
deriving (Eq, Ord, Show)
moduleInfoFromSrcLoc :: SrcLoc -> ModuleInfo
moduleInfoFromSrcLoc = ModuleInfo . srcLocModule
-- | Produce 'ModuleInfo' from the top location on the Haskell call stack (i.e. the file where the call to 'moduleInfoFromCallStack' was made).
moduleInfoFromCallStack :: HasCallStack => ModuleInfo
moduleInfoFromCallStack = maybe (ModuleInfo "?") (moduleInfoFromSrcLoc . snd) (listToMaybe (getCallStack callStack))

View File

@ -79,4 +79,4 @@ showCallStack :: Colourize -> CallStack -> ShowS
showCallStack colourize callStack = foldr (.) id (intersperse (showChar '\n') (uncurry (showCallSite colourize) <$> getCallStack callStack))
showCallSite :: Colourize -> String -> SrcLoc -> ShowS
showCallSite colourize symbol SrcLoc{..} = showString symbol . showChar ' ' . withSGRCode colourize [SetConsoleIntensity BoldIntensity] (showParen True (showSpan (Just srcLocFile) (Span (Pos srcLocStartLine srcLocStartCol) (Pos srcLocEndLine srcLocEndCol))))
showCallSite colourize symbol loc@SrcLoc{..} = showString symbol . showChar ' ' . withSGRCode colourize [SetConsoleIntensity BoldIntensity] (showParen True (showSpan (Just srcLocFile) (spanFromSrcLoc loc)))

View File

@ -6,6 +6,7 @@
module Data.Span
( Span(..)
, Pos(..)
, spanFromSrcLoc
, emptySpan
) where
@ -13,6 +14,7 @@ import Data.Aeson ((.=), (.:))
import qualified Data.Aeson as A
import Data.JSON.Fields
import Data.Semilattice.Lower
import GHC.Stack
import Prologue
-- | Source position information
@ -37,6 +39,9 @@ data Span = Span
}
deriving (Show, Read, Eq, Ord, Generic, Hashable)
spanFromSrcLoc :: SrcLoc -> Span
spanFromSrcLoc = Span . (Pos . srcLocStartLine <*> srcLocStartCol) <*> (Pos . srcLocEndLine <*> srcLocEndCol)
emptySpan :: Span
emptySpan = Span (Pos 1 1) (Pos 1 1)

View File

@ -1,6 +1,7 @@
{-# LANGUAGE OverloadedLists #-}
module Analysis.Go.Spec (spec) where
import Data.Abstract.Environment as Env
import Data.Abstract.Evaluatable (EvalError(..))
import qualified Language.Go.Assignment as Go
import qualified Data.Language as Language
@ -12,22 +13,16 @@ spec :: Spec
spec = parallel $ do
describe "evaluates Go" $ do
it "imports and wildcard imports" $ do
res <- snd <$> evaluate "main.go"
environment res `shouldBe` [ ("foo", addr 0)
, ("Bar", addr 2)
, ("Rab", addr 3)
, ("main", addr 4)
]
((_, state), _) <- evaluate "main.go"
Env.names (environment state) `shouldBe` [ "Bar", "Rab", "foo", "main" ]
heapLookup (Address (Precise 0)) (heap res) `shouldBe` ns "foo" [ ("New", addr 1) ]
(derefQName (heap state) ("foo" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("foo", ["New"])
it "imports with aliases (and side effects only)" $ do
res <- snd <$> evaluate "main1.go"
environment res `shouldBe` [ ("f", addr 0)
, ("main", addr 4) -- addr 4 is due to side effects of eval'ing `import _ "./bar"` which used addr 2 & 3. f defines New which got addr 1.
]
((_, state), _) <- evaluate "main1.go"
Env.names (environment state) `shouldBe` [ "f", "main" ]
heapLookup (Address (Precise 0)) (heap res) `shouldBe` ns "f" [ ("New", addr 1) ]
(derefQName (heap state) ("f" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("f", ["New"])
where
fixtures = "test/fixtures/go/analysis/"

View File

@ -1,6 +1,7 @@
{-# LANGUAGE OverloadedLists #-}
module Analysis.PHP.Spec (spec) where
import Data.Abstract.Environment as Env
import Data.Abstract.Evaluatable (EvalError(..))
import qualified Language.PHP.Assignment as PHP
import qualified Data.Language as Language
@ -12,26 +13,20 @@ spec :: Spec
spec = parallel $ do
describe "PHP" $ do
it "evaluates include and require" $ do
env <- environment . snd <$> evaluate "main.php"
env `shouldBe` [ ("foo", addr 0)
, ("bar", addr 1) ]
env <- environment . snd . fst <$> evaluate "main.php"
Env.names env `shouldBe` [ "bar", "foo" ]
it "evaluates include_once and require_once" $ do
env <- environment . snd <$> evaluate "main_once.php"
env `shouldBe` [ ("foo", addr 0)
, ("bar", addr 1) ]
env <- environment . snd . fst <$> evaluate "main_once.php"
Env.names env `shouldBe` [ "bar", "foo" ]
it "evaluates namespaces" $ do
res <- snd <$> evaluate "namespaces.php"
environment res `shouldBe` [ ("NS1", addr 0)
, ("Foo", addr 6) ]
((_, state), _) <- evaluate "namespaces.php"
Env.names (environment state) `shouldBe` [ "Foo", "NS1" ]
heapLookup (Address (Precise 0)) (heap res) `shouldBe` ns "NS1" [ ("Sub1", addr 1)
, ("b", addr 4)
, ("c", addr 5)
]
heapLookup (Address (Precise 1)) (heap res) `shouldBe` ns "Sub1" [ ("Sub2", addr 2) ]
heapLookup (Address (Precise 2)) (heap res) `shouldBe` ns "Sub2" [ ("f", addr 3) ]
(derefQName (heap state) ("NS1" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("NS1", ["Sub1", "b", "c"])
(derefQName (heap state) ("NS1" :| ["Sub1"]) (environment state) >>= deNamespace) `shouldBe` Just ("Sub1", ["Sub2"])
(derefQName (heap state) ("NS1" :| ["Sub1", "Sub2"]) (environment state) >>= deNamespace) `shouldBe` Just ("Sub2", ["f"])
where
fixtures = "test/fixtures/php/analysis/"

View File

@ -1,6 +1,7 @@
{-# LANGUAGE OverloadedLists, OverloadedStrings #-}
module Analysis.Python.Spec (spec) where
import Data.Abstract.Environment as Env
import Data.Abstract.Evaluatable (EvalError(..))
import Data.Abstract.Value
import Data.Map
@ -14,37 +15,28 @@ spec :: Spec
spec = parallel $ do
describe "evaluates Python" $ do
it "imports" $ do
res <- snd <$> evaluate "main.py"
environment res `shouldBe` [ ("print", addr 0)
, ("a", addr 1)
, ("b", addr 3)
]
((_, state), _) <- evaluate "main.py"
Env.names (environment state) `shouldContain` [ "a", "b" ]
heapLookup (Address (Precise 1)) (heap res) `shouldBe` ns "a" [ ("foo", addr 2) ]
heapLookup (Address (Precise 3)) (heap res) `shouldBe` ns "b" [ ("c", addr 4) ]
heapLookup (Address (Precise 4)) (heap res) `shouldBe` ns "c" [ ("baz", addr 5) ]
(derefQName (heap state) ("a" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("a", ["foo"])
(derefQName (heap state) ("b" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("b", ["c"])
(derefQName (heap state) ("b" :| ["c"]) (environment state) >>= deNamespace) `shouldBe` Just ("c", ["baz"])
it "imports with aliases" $ do
env <- environment . snd <$> evaluate "main1.py"
env `shouldBe` [ ("print", addr 0)
, ("b", addr 1)
, ("e", addr 3)
]
env <- environment . snd . fst <$> evaluate "main1.py"
Env.names env `shouldContain` [ "b", "e" ]
it "imports using 'from' syntax" $ do
env <- environment . snd <$> evaluate "main2.py"
env `shouldBe` [ ("print", addr 0)
, ("foo", addr 1)
, ("bar", addr 2)
]
env <- environment . snd . fst <$> evaluate "main2.py"
Env.names env `shouldContain` [ "bar", "foo" ]
it "subclasses" $ do
v <- fst <$> evaluate "subclass.py"
v `shouldBe` Right [injValue (String "\"bar\"")]
((res, _), _) <- evaluate "subclass.py"
res `shouldBe` Right [injValue (String "\"bar\"")]
it "handles multiple inheritance left-to-right" $ do
v <- fst <$> evaluate "multiple_inheritance.py"
v `shouldBe` Right [injValue (String "\"foo!\"")]
((res, _), _) <- evaluate "multiple_inheritance.py"
res `shouldBe` Right [injValue (String "\"foo!\"")]
where
ns n = Just . Latest . Just . injValue . Namespace n

View File

@ -2,6 +2,7 @@
module Analysis.Ruby.Spec (spec) where
import Data.Abstract.Environment as Env
import Data.Abstract.Evaluatable
import Data.Abstract.Value as Value
import Data.Abstract.Number as Number
@ -20,62 +21,60 @@ spec :: Spec
spec = parallel $ do
describe "Ruby" $ do
it "evaluates require_relative" $ do
env <- environment . snd <$> evaluate "main.rb"
env `shouldBe` [ ("Object", addr 0)
, ("foo", addr 3) ]
((res, state), _) <- evaluate "main.rb"
res `shouldBe` Right [injValue (Value.Integer (Number.Integer 1))]
Env.names (environment state) `shouldContain` ["foo"]
it "evaluates load" $ do
env <- environment . snd <$> evaluate "load.rb"
env `shouldBe` [ ("Object", addr 0)
, ("foo", addr 3) ]
env <- environment . snd . fst <$> evaluate "load.rb"
Env.names env `shouldContain` ["foo"]
it "evaluates load with wrapper" $ do
res <- evaluate "load-wrap.rb"
fst res `shouldBe` Left (SomeExc (injectSum @(EnvironmentError (Value Precise)) (FreeVariable "foo")))
environment (snd res) `shouldBe` [ ("Object", addr 0) ]
((res, state), _) <- evaluate "load-wrap.rb"
res `shouldBe` Left (SomeExc (injectSum @(EnvironmentError (Value Precise)) (FreeVariable "foo")))
Env.names (environment state) `shouldContain` [ "Object" ]
it "evaluates subclass" $ do
res <- evaluate "subclass.rb"
fst res `shouldBe` Right [injValue (String "\"<bar>\"")]
environment (snd res) `shouldBe` [ ("Bar", addr 6)
, ("Foo", addr 3)
, ("Object", addr 0) ]
((res, state), _) <- evaluate "subclass.rb"
res `shouldBe` Right [injValue (String "\"<bar>\"")]
Env.names (environment state) `shouldContain` [ "Bar", "Foo" ]
heapLookup (Address (Precise 6)) (heap (snd res))
`shouldBe` ns "Bar" [ ("baz", addr 8)
, ("foo", addr 5)
, ("inspect", addr 7) ]
(derefQName (heap state) ("Bar" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("Bar", ["baz", "foo", "inspect"])
it "evaluates modules" $ do
res <- evaluate "modules.rb"
fst res `shouldBe` Right [injValue (String "\"<hello>\"")]
environment (snd res) `shouldBe` [ ("Object", addr 0)
, ("Bar", addr 3) ]
((res, state), _) <- evaluate "modules.rb"
res `shouldBe` Right [injValue (String "\"<hello>\"")]
Env.names (environment state) `shouldContain` [ "Bar" ]
it "handles break correctly" $ do
res <- evaluate "break.rb"
fst res `shouldBe` Right [injValue (Value.Integer (Number.Integer 3))]
((res, _), _) <- evaluate "break.rb"
res `shouldBe` Right [injValue (Value.Integer (Number.Integer 3))]
it "handles break correctly" $ do
res <- evaluate "next.rb"
fst res `shouldBe` Right [injValue (Value.Integer (Number.Integer 8))]
((res, _), _) <- evaluate "next.rb"
res `shouldBe` Right [injValue (Value.Integer (Number.Integer 8))]
it "calls functions with arguments" $ do
res <- evaluate "call.rb"
fst res `shouldBe` Right [injValue (Value.Integer (Number.Integer 579))]
((res, _), _) <- evaluate "call.rb"
res `shouldBe` Right [injValue (Value.Integer (Number.Integer 579))]
it "evaluates early return statements" $ do
res <- evaluate "early-return.rb"
fst res `shouldBe` Right [injValue (Value.Integer (Number.Integer 123))]
((res, _), _) <- evaluate "early-return.rb"
res `shouldBe` Right [injValue (Value.Integer (Number.Integer 123))]
it "has prelude" $ do
res <- fst <$> evaluate "preluded.rb"
((res, _), _) <- evaluate "preluded.rb"
res `shouldBe` Right [injValue (String "\"<foo>\"")]
it "evaluates __LINE__" $ do
res <- fst <$> evaluate "line.rb"
((res, _), _) <- evaluate "line.rb"
res `shouldBe` Right [injValue (Value.Integer (Number.Integer 4))]
it "resolves builtins used in the prelude" $ do
((res, _), traces) <- evaluate "puts.rb"
res `shouldBe` Right [injValue Unit]
traces `shouldContain` [ "\"hello\"" ]
where
ns n = Just . Latest . Just . injValue . Namespace n
addr = Address . Precise

View File

@ -1,6 +1,8 @@
{-# LANGUAGE OverloadedLists #-}
module Analysis.TypeScript.Spec (spec) where
import Control.Arrow ((&&&))
import Data.Abstract.Environment as Env
import Data.Abstract.Evaluatable
import qualified Language.TypeScript.Assignment as TypeScript
import Data.Abstract.Value as Value
@ -14,32 +16,27 @@ spec :: Spec
spec = parallel $ do
describe "evaluates TypeScript" $ do
it "imports with aliased symbols" $ do
env <- environment . snd <$> evaluate "main.ts"
env `shouldBe` [ ("bar", addr 0)
, ("quz", addr 3)]
env <- environment . snd . fst <$> evaluate "main.ts"
Env.names env `shouldBe` [ "bar", "quz" ]
it "imports with qualified names" $ do
res <- snd <$> evaluate "main1.ts"
environment res `shouldBe` [ ("b", addr 0)
, ("z", addr 4)
]
((_, state), _) <- evaluate "main1.ts"
Env.names (environment state) `shouldBe` [ "b", "z" ]
heapLookup (Address (Precise 0)) (heap res) `shouldBe` ns "b" [ ("baz", addr 1)
, ("foo", addr 3) ]
heapLookup (Address (Precise 4)) (heap res) `shouldBe` ns "z" [ ("baz", addr 1)
, ("foo", addr 3) ]
(derefQName (heap state) ("b" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("b", [ "baz", "foo" ])
(derefQName (heap state) ("z" :| []) (environment state) >>= deNamespace) `shouldBe` Just ("z", [ "baz", "foo" ])
it "side effect only imports" $ do
env <- environment . snd <$> evaluate "main2.ts"
env <- environment . snd . fst <$> evaluate "main2.ts"
env `shouldBe` emptyEnv
it "fails exporting symbols not defined in the module" $ do
v <- fst <$> evaluate "bad-export.ts"
v `shouldBe` Left (SomeExc (injectSum (ExportError "foo.ts" (Name "pip") :: EvalError (Value Precise) ())))
((res, _), _) <- evaluate "bad-export.ts"
res `shouldBe` Left (SomeExc (injectSum @(EvalError (Value Precise)) (ExportError "foo.ts" (Name "pip"))))
it "evaluates early return statements" $ do
res <- evaluate "early-return.ts"
fst res `shouldBe` Right [injValue (Value.Float (Number.Decimal 123.0))]
((res, _), _) <- evaluate "early-return.ts"
res `shouldBe` Right [injValue (Value.Float (Number.Decimal 123.0))]
where
fixtures = "test/fixtures/typescript/analysis/"

View File

@ -40,9 +40,12 @@ evaluate
. runReturn
. runLoopControl
. fmap fst
. runGoto lowerBound
. runState (Gotos lowerBound)
. runGoto Gotos getGotos
. constraining
newtype Gotos effects = Gotos { getGotos :: GotoTable (State (Gotos effects) ': effects) (Value Precise) }
constraining :: Evaluator Precise (Value Precise) effects a -> Evaluator Precise (Value Precise) effects a
constraining = id

View File

@ -4,8 +4,8 @@ module SpecHelpers
, parseFilePath
, readFilePair
, testEvaluating
, ns
, addr
, deNamespace
, derefQName
, verbatim
, Verbatim(..)
) where
@ -14,18 +14,22 @@ import Analysis.Abstract.Evaluating
import Analysis.Abstract.Evaluating as X (EvaluatingState(..))
import Control.Abstract.Addressable
import Control.Abstract.Value
import Control.Arrow ((&&&))
import Control.Effect as X (runIgnoringTraces)
import Control.Monad ((>=>))
import Data.Abstract.Address as X
import Data.Abstract.Environment as Env
import Data.Abstract.Evaluatable
import Data.Abstract.FreeVariables as X hiding (dropExtension)
import Data.Abstract.Heap as X
import Data.Abstract.ModuleTable as X hiding (lookup)
import Data.Abstract.Value (Namespace(..), Value, ValueError, injValue, runValueError)
import Data.Abstract.Value (Namespace(..), Value, ValueError, injValue, prjValue, runValueError)
import Data.Bifunctor (first)
import Data.Blob as X
import Data.File as X
import Data.Functor.Listable as X
import Data.Language as X
import Data.List.NonEmpty as X (NonEmpty(..))
import Data.Output as X
import Data.Range as X
import Data.Record as X
@ -70,9 +74,9 @@ readFilePair paths = let paths' = fmap file paths in
testEvaluating
= run
. runReturningTraces
. fmap (first reassociate)
. evaluating
. runIgnoringTraces
. runLoadError
. runValueError
. runUnspecialized
@ -82,8 +86,14 @@ testEvaluating
. runAddressError
. constrainedToValuePrecise
ns n = Just . Latest . Just . injValue . Namespace n
addr = Address . Precise
deNamespace :: Value Precise -> Maybe (Name, [Name])
deNamespace = fmap (namespaceName &&& Env.names . namespaceScope) . prjValue @(Namespace Precise)
derefQName :: Heap Precise (Value Precise) -> NonEmpty Name -> Environment Precise (Value Precise) -> Maybe (Value Precise)
derefQName heap = go
where go (n1 :| ns) env = Env.lookup n1 env >>= flip heapLookup heap >>= unLatest >>= case ns of
[] -> Just
(n2 : ns) -> fmap namespaceScope . prjValue @(Namespace Precise) >=> go (n2 :| ns)
newtype Verbatim = Verbatim ByteString
deriving (Eq)

1
test/fixtures/ruby/analysis/puts.rb vendored Normal file
View File

@ -0,0 +1 @@
puts "hello"