1
1
mirror of https://github.com/anoma/juvix.git synced 2024-11-30 14:13:27 +03:00

Use JuvixError instead of Text for errors in Package file loading (#2459)

Depends on:
*  https://github.com/anoma/juvix/pull/2458

This PR is part of a series implementing:
* https://github.com/anoma/juvix/issues/2336

In attempt to make the main PR:
* https://github.com/anoma/juvix/pull/2434
easier to review.

This PR introduces standard JuvixError handling for errors related to
the loading of the juvix.yaml file. Before this PR errors were thrown as
Text and then communicated to the user using the `error` function.
This commit is contained in:
Paul Cadman 2023-10-23 19:01:36 +01:00 committed by GitHub
parent 7b7f06f81a
commit 8e6c1c8f07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 320 additions and 47 deletions

View File

@ -147,7 +147,7 @@ loadDefaultPrelude = whenJustM defaultPreludeEntryPoint $ \e -> do
. runM
. evalInternet hasInternet
. runFilesIO
. runError @Text
. runError @JuvixError
. runReader e
. runLogIO
. runProcessIO

View File

@ -30,7 +30,7 @@ import Juvix.Prelude
mkPackage ::
forall r.
(Members '[Files, Error Text, Reader ResolverEnv, GitClone] r) =>
(Members '[Files, Error JuvixError, Reader ResolverEnv, GitClone] r) =>
Maybe EntryPoint ->
Path Abs Dir ->
Sem r Package
@ -43,7 +43,7 @@ mkPackage mpackageEntry _packageRoot = do
mkPackageInfo ::
forall r.
(Members '[Files, Error Text, Reader ResolverEnv, Error DependencyError, GitClone] r) =>
(Members '[Files, Error JuvixError, Reader ResolverEnv, Error DependencyError, GitClone] r) =>
Maybe EntryPoint ->
Path Abs Dir ->
Package ->
@ -162,7 +162,7 @@ resolveDependency i = case i ^. packageDepdendencyInfoDependency of
registerDependencies' ::
forall r.
(Members '[Reader EntryPoint, State ResolverState, Reader ResolverEnv, Files, Error Text, Error DependencyError, GitClone] r) =>
(Members '[Reader EntryPoint, State ResolverState, Reader ResolverEnv, Files, Error JuvixError, Error DependencyError, GitClone] r) =>
DependenciesConfig ->
Sem r ()
registerDependencies' conf = do
@ -186,7 +186,7 @@ registerDependencies' conf = do
addRootDependency ::
forall r.
(Members '[State ResolverState, Reader ResolverEnv, Files, Error Text, Error DependencyError, GitClone] r) =>
(Members '[State ResolverState, Reader ResolverEnv, Files, Error JuvixError, Error DependencyError, GitClone] r) =>
DependenciesConfig ->
EntryPoint ->
Path Abs Dir ->
@ -207,7 +207,7 @@ addRootDependency conf e root = do
addDependency ::
forall r.
(Members '[State ResolverState, Reader ResolverEnv, Files, Error Text, Error DependencyError, GitClone] r) =>
(Members '[State ResolverState, Reader ResolverEnv, Files, Error JuvixError, Error DependencyError, GitClone] r) =>
Maybe EntryPoint ->
PackageDependencyInfo ->
Sem r LockfileDependency
@ -224,7 +224,7 @@ addDependency me d = do
addDependency' ::
forall r.
(Members '[State ResolverState, Reader ResolverEnv, Files, Error Text, Error DependencyError, GitClone] r) =>
(Members '[State ResolverState, Reader ResolverEnv, Files, Error JuvixError, Error DependencyError, GitClone] r) =>
Package ->
Maybe EntryPoint ->
ResolvedDependency ->
@ -319,7 +319,7 @@ expectedPath' actualPath m = do
re ::
forall r a.
(Members '[Reader EntryPoint, Files, Error Text, Error DependencyError, GitClone] r) =>
(Members '[Reader EntryPoint, Files, Error JuvixError, Error DependencyError, GitClone] r) =>
Sem (PathResolver ': r) a ->
Sem (Reader ResolverEnv ': State ResolverState ': r) a
re = reinterpret2H helper
@ -342,13 +342,13 @@ re = reinterpret2H helper
Right (r, _) -> r
raise (evalPathResolver' st' root' (a' x'))
evalPathResolver' :: (Members '[Reader EntryPoint, Files, Error Text, Error DependencyError, GitClone] r) => ResolverState -> Path Abs Dir -> Sem (PathResolver ': r) a -> Sem r a
evalPathResolver' :: (Members '[Reader EntryPoint, Files, Error JuvixError, Error DependencyError, GitClone] r) => ResolverState -> Path Abs Dir -> Sem (PathResolver ': r) a -> Sem r a
evalPathResolver' st root = fmap snd . runPathResolver' st root
runPathResolver :: (Members '[Reader EntryPoint, Files, Error Text, Error DependencyError, GitClone] r) => Path Abs Dir -> Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolver :: (Members '[Reader EntryPoint, Files, Error JuvixError, Error DependencyError, GitClone] r) => Path Abs Dir -> Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolver = runPathResolver' iniResolverState
runPathResolver' :: (Members '[Reader EntryPoint, Files, Error Text, Error DependencyError, GitClone] r) => ResolverState -> Path Abs Dir -> Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolver' :: (Members '[Reader EntryPoint, Files, Error JuvixError, Error DependencyError, GitClone] r) => ResolverState -> Path Abs Dir -> Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolver' st root x = do
e <- ask
let _envSingleFile :: Maybe (Path Abs File)
@ -364,15 +364,15 @@ runPathResolver' st root x = do
}
runState st (runReader env (re x))
runPathResolverPipe' :: (Members '[Files, Reader EntryPoint, Error DependencyError, GitClone] r) => ResolverState -> Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolverPipe' :: (Members '[Files, Reader EntryPoint, Error DependencyError, GitClone, Error JuvixError] r) => ResolverState -> Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolverPipe' iniState a = do
r <- asks (^. entryPointResolverRoot)
runError (runPathResolver' iniState r (raiseUnder a)) >>= either error return
runPathResolver' iniState r a
runPathResolverPipe :: (Members '[Files, Reader EntryPoint, Error DependencyError, GitClone] r) => Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolverPipe :: (Members '[Files, Reader EntryPoint, Error DependencyError, GitClone, Error JuvixError] r) => Sem (PathResolver ': r) a -> Sem r (ResolverState, a)
runPathResolverPipe a = do
r <- asks (^. entryPointResolverRoot)
runError (runPathResolver r (raiseUnder a)) >>= either error return
runPathResolver r a
evalPathResolverPipe :: (Members '[Files, Reader EntryPoint, Error DependencyError, GitClone] r) => Sem (PathResolver ': r) a -> Sem r a
evalPathResolverPipe :: (Members '[Files, Reader EntryPoint, Error DependencyError, GitClone, Error JuvixError] r) => Sem (PathResolver ': r) a -> Sem r a
evalPathResolverPipe = fmap snd . runPathResolverPipe

View File

@ -6,5 +6,5 @@ import Juvix.Compiler.Pipeline.EntryPoint
import Juvix.Data.Effect.Git
import Juvix.Prelude
runPathResolverArtifacts :: (Members '[Files, Reader EntryPoint, State Artifacts, Error DependencyError, GitClone] r) => Sem (PathResolver ': r) a -> Sem r a
runPathResolverArtifacts :: (Members '[Files, Reader EntryPoint, State Artifacts, Error DependencyError, GitClone, Error JuvixError] r) => Sem (PathResolver ': r) a -> Sem r a
runPathResolverArtifacts = runStateLikeArtifacts runPathResolverPipe' artifactResolver

View File

@ -10,6 +10,7 @@ import Data.String.Interpolate (i)
import Data.Yaml
import Data.Yaml.Pretty
import Juvix.Compiler.Pipeline.Package.Dependency
import Juvix.Compiler.Pipeline.Package.Loader.Error
import Juvix.Extra.Paths
import Juvix.Extra.Strings qualified as Str
import Juvix.Extra.Version
@ -84,21 +85,33 @@ mkPackageLockfilePath = (<//> juvixLockfile)
mayReadLockfile ::
forall r.
(Members '[Files, Error Text] r) =>
(Members '[Files, Error PackageLoaderError] r) =>
Path Abs Dir ->
Sem r (Maybe LockfileInfo)
mayReadLockfile root = do
let lockfilePath = mkPackageLockfilePath root
lockfileExists <- fileExists' lockfilePath
if
| lockfileExists -> do
bs <- readFileBS' lockfilePath
either (throw . pack . prettyPrintParseException) ((return . Just) . mkLockfileInfo lockfilePath) (decodeEither' @Lockfile bs)
either (throwErr . pack . prettyPrintParseException) ((return . Just) . mkLockfileInfo lockfilePath) (decodeEither' @Lockfile bs)
| otherwise -> return Nothing
where
mkLockfileInfo :: Path Abs File -> Lockfile -> LockfileInfo
mkLockfileInfo _lockfileInfoPath _lockfileInfoLockfile = LockfileInfo {..}
lockfilePath :: Path Abs File
lockfilePath = mkPackageLockfilePath root
throwErr :: Text -> Sem r a
throwErr e =
throw
PackageLoaderError
{ _packageLoaderErrorPath = lockfilePath,
_packageLoaderErrorCause =
ErrLockfileYamlParseError
LockfileYamlParseError {_lockfileYamlParseErrorError = e}
}
lockfileEncodeConfig :: Config
lockfileEncodeConfig = setConfCompare keyCompare defConfig
where

View File

@ -13,10 +13,11 @@ import Data.Versions
import Data.Yaml
import Juvix.Compiler.Pipeline.Lockfile
import Juvix.Compiler.Pipeline.Package.Base
import Juvix.Compiler.Pipeline.Package.Loader.Error
import Juvix.Extra.Paths
import Juvix.Prelude
processPackage :: forall r. (Members '[Error Text] r) => Path Abs File -> BuildDir -> Maybe LockfileInfo -> RawPackage -> Sem r Package
processPackage :: forall r. (Members '[Error PackageLoaderError] r) => Path Abs File -> BuildDir -> Maybe LockfileInfo -> RawPackage -> Sem r Package
processPackage _packageFile buildDir lockfile pkg = do
let _packageName = fromMaybe defaultPackageName (pkg ^. packageName)
_packageDependencies = resolveDependencies
@ -35,7 +36,14 @@ processPackage _packageFile buildDir lockfile pkg = do
Nothing -> return defaultVersion
Just ver -> case semver ver of
Right v -> return v
Left err -> throw (pack (errorBundlePretty err))
Left err ->
throw
PackageLoaderError
{ _packageLoaderErrorCause =
ErrVersionParseError
VersionParseError {_versionParseErrorError = (pack (errorBundlePretty err))},
_packageLoaderErrorPath = _packageFile
}
checkNoDuplicateDepNames :: [Dependency] -> Sem r ()
checkNoDuplicateDepNames deps = go HashSet.empty (deps ^.. traversed . _GitDependency . gitDependencyName)
@ -43,15 +51,17 @@ processPackage _packageFile buildDir lockfile pkg = do
go :: HashSet Text -> [Text] -> Sem r ()
go _ [] = return ()
go s (x : xs)
| x `HashSet.member` s = throw (errMsg x)
| x `HashSet.member` s =
throw
PackageLoaderError
{ _packageLoaderErrorPath = _packageFile,
_packageLoaderErrorCause =
ErrDuplicateDependencyError
DuplicateDependencyError
{ _duplicateDependencyErrorName = x
}
}
| otherwise = go (HashSet.insert x s) xs
where
errMsg :: Text -> Text
errMsg dupName =
"Juvix package file at: "
<> pack (toFilePath _packageFile)
<> " contains the duplicate dependency name: "
<> dupName
resolveDependencies :: [Dependency]
resolveDependencies = fromMaybe [stdlib] (pkg ^. packageDependencies)
@ -62,36 +72,37 @@ processPackage _packageFile buildDir lockfile pkg = do
-- | Given some directory d it tries to read the file d/juvix.yaml and parse its contents
readPackage ::
forall r.
(Members '[Files, Error Text] r) =>
(Members '[Files, Error JuvixError] r) =>
Path Abs Dir ->
BuildDir ->
Sem r Package
readPackage root buildDir = do
readPackage root buildDir = mapError (JuvixError @PackageLoaderError) $ do
bs <- readFileBS' yamlPath
mLockfile <- mayReadLockfile root
if
| ByteString.null bs -> return (emptyPackage buildDir yamlPath)
| otherwise -> either (throw . pack . prettyPrintParseException) (processPackage yamlPath buildDir mLockfile) (decodeEither' bs)
| otherwise -> either (throwErr . pack . prettyPrintParseException) (processPackage yamlPath buildDir mLockfile) (decodeEither' bs)
where
yamlPath = mkPackageFilePath root
throwErr e =
throw
PackageLoaderError
{ _packageLoaderErrorPath = yamlPath,
_packageLoaderErrorCause =
ErrPackageYamlParseError
PackageYamlParseError
{ _packageYamlParseErrorError = e
}
}
readPackageIO :: Path Abs Dir -> BuildDir -> IO Package
readPackageIO root buildDir = do
let x :: Sem '[Error Text, Files, Embed IO] Package
x = readPackage root buildDir
m <- runM $ runFilesIO (runError x)
case m of
Left err -> putStrLn err >> exitFailure
Right r -> return r
readPackageIO root buildDir = runM (runFilesIO (runErrorIO' @JuvixError (readPackage root buildDir)))
readGlobalPackageIO :: IO Package
readGlobalPackageIO = do
m <- runM . runFilesIO . runError $ readGlobalPackage
case m of
Left err -> putStrLn err >> exitFailure
Right r -> return r
readGlobalPackageIO = runM (runFilesIO . runErrorIO' @JuvixError $ readGlobalPackage)
readGlobalPackage :: (Members '[Error Text, Files] r) => Sem r Package
readGlobalPackage :: (Members '[Error JuvixError, Files] r) => Sem r Package
readGlobalPackage = do
yamlPath <- globalYaml
unlessM (fileExists' yamlPath) writeGlobalPackage

View File

@ -0,0 +1,85 @@
module Juvix.Compiler.Pipeline.Package.Loader.Error where
import Juvix.Data.CodeAnn
import Juvix.Prelude
data PackageLoaderError = PackageLoaderError
{ _packageLoaderErrorPath :: Path Abs File,
_packageLoaderErrorCause :: PackageLoaderErrorCause
}
data PackageLoaderErrorCause
= ErrPackageYamlParseError PackageYamlParseError
| ErrLockfileYamlParseError LockfileYamlParseError
| ErrVersionParseError VersionParseError
| ErrDuplicateDependencyError DuplicateDependencyError
newtype PackageYamlParseError = PackageYamlParseError
{ _packageYamlParseErrorError :: Text
}
newtype LockfileYamlParseError = LockfileYamlParseError
{ _lockfileYamlParseErrorError :: Text
}
newtype VersionParseError = VersionParseError
{ _versionParseErrorError :: Text
}
newtype DuplicateDependencyError = DuplicateDependencyError
{ _duplicateDependencyErrorName :: Text
}
makeLenses ''PackageLoaderError
makeLenses ''PackageYamlParseError
makeLenses ''LockfileYamlParseError
makeLenses ''VersionParseError
makeLenses ''DuplicateDependencyError
instance ToGenericError PackageLoaderError where
genericError e = do
let msg = mkAnsiText (ppCodeAnn e)
return
GenericError
{ _genericErrorMessage = msg,
_genericErrorLoc = i,
_genericErrorIntervals = [i]
}
where
i = getLoc e
instance PrettyCodeAnn PackageLoaderError where
ppCodeAnn e = ppCodeAnn (e ^. packageLoaderErrorCause)
instance PrettyCodeAnn PackageLoaderErrorCause where
ppCodeAnn = \case
ErrPackageYamlParseError e -> ppCodeAnn e
ErrLockfileYamlParseError e -> ppCodeAnn e
ErrVersionParseError e -> ppCodeAnn e
ErrDuplicateDependencyError e -> ppCodeAnn e
instance PrettyCodeAnn PackageYamlParseError where
ppCodeAnn e =
"The package file is invalid"
<> line
<+> pretty (e ^. packageYamlParseErrorError)
instance PrettyCodeAnn LockfileYamlParseError where
ppCodeAnn e =
"The lock file is invalid"
<> line
<+> pretty (e ^. lockfileYamlParseErrorError)
instance PrettyCodeAnn VersionParseError where
ppCodeAnn e =
"The package version is invalid"
<> line
<+> pretty (e ^. versionParseErrorError)
instance PrettyCodeAnn DuplicateDependencyError where
ppCodeAnn e =
"Juvix package file contains the duplicate dependency name:"
<+> pretty (e ^. duplicateDependencyErrorName)
instance HasLoc PackageLoaderError where
getLoc e = singletonInterval (mkInitialLoc (e ^. packageLoaderErrorPath))

View File

@ -10,6 +10,7 @@ import Examples qualified
import Format qualified
import Formatter qualified
import Internal qualified
import Package qualified
import Parsing qualified
import Reachability qualified
import Runtime qualified
@ -43,7 +44,8 @@ fastTests =
Typecheck.allTests,
Reachability.allTests,
Format.allTests,
Formatter.allTests
Formatter.allTests,
Package.allTests
]
main :: IO ()

11
test/Package.hs Normal file
View File

@ -0,0 +1,11 @@
module Package
( allTests,
)
where
import Base
import Package.Negative qualified as N
import Package.Positive qualified as P
allTests :: TestTree
allTests = testGroup "Package loading tests" [N.allTests, P.allTests]

68
test/Package/Negative.hs Normal file
View File

@ -0,0 +1,68 @@
module Package.Negative where
import Base
import Juvix.Compiler.Pipeline.Package
import Juvix.Compiler.Pipeline.Package.Loader.Error
type FailMsg = String
data NegTest a = NegTest
{ _name :: String,
_relDir :: Path Rel Dir,
_checkErr :: a -> Maybe FailMsg
}
root :: Path Abs Dir
root = relToProject $(mkRelDir "tests/negative/Package")
testDescr :: (Typeable a) => NegTest a -> TestDescr
testDescr NegTest {..} =
let tRoot = root <//> _relDir
in TestDescr
{ _testName = _name,
_testRoot = tRoot,
_testAssertion = Single $ do
res <- withTempDir' (runM . runError . runFilesIO . readPackage tRoot . CustomBuildDir . Abs)
case mapLeft fromJuvixError res of
Left (Just err) -> whenJust (_checkErr err) assertFailure
Left Nothing -> assertFailure "An error ocurred but it was not when reading the package."
Right {} -> assertFailure "There was no error when reading the package"
}
allTests :: TestTree
allTests =
testGroup
"Package loading negative tests"
( map (mkTest . testDescr) packageErrorTests
)
wrongError :: Maybe FailMsg
wrongError = Just "Incorrect error"
packageErrorTests :: [NegTest PackageLoaderError]
packageErrorTests =
[ NegTest
"package YAML parse error"
$(mkRelDir "YamlParseError")
$ \case
PackageLoaderError _ ErrPackageYamlParseError {} -> Nothing
_ -> wrongError,
NegTest
"lockfile YAML parse error"
$(mkRelDir "InvalidLockfile")
$ \case
PackageLoaderError _ ErrLockfileYamlParseError {} -> Nothing
_ -> wrongError,
NegTest
"package YAML invalid version"
$(mkRelDir "YamlInvalidVersion")
$ \case
PackageLoaderError _ ErrVersionParseError {} -> Nothing
_ -> wrongError,
NegTest
"package YAML duplicate dependencies"
$(mkRelDir "YamlDuplicateDependencies")
$ \case
PackageLoaderError _ ErrDuplicateDependencyError {} -> Nothing
_ -> wrongError
]

64
test/Package/Positive.hs Normal file
View File

@ -0,0 +1,64 @@
module Package.Positive where
import Base
import Juvix.Compiler.Pipeline.Package
type FailMsg = String
data PosTest = PosTest
{ _name :: String,
_relDir :: Path Rel Dir,
_checkPackage :: Package -> BuildDir -> Maybe FailMsg
}
root :: Path Abs Dir
root = relToProject $(mkRelDir "tests/positive/PackageLoader")
testDescr :: PosTest -> TestDescr
testDescr PosTest {..} =
let tRoot = root <//> _relDir
in TestDescr
{ _testName = _name,
_testRoot = tRoot,
_testAssertion = Single $ do
withTempDir' $ \d -> do
let buildDir = CustomBuildDir (Abs d)
res <- runM . runError @JuvixError . runFilesIO . readPackage tRoot $ buildDir
case res of
Right p -> whenJust (_checkPackage p buildDir) assertFailure
Left {} -> assertFailure "An error ocurred when reading the package."
}
allTests :: TestTree
allTests =
testGroup
"Package loading positive tests"
( map (mkTest . testDescr) packageLoadingTests
)
packageLoadingTests :: [PosTest]
packageLoadingTests =
[ PosTest
"empty YAML is valid"
$(mkRelDir "YamlEmpty")
$ \p _ ->
if
| p ^. packageName == defaultPackageName -> Nothing
| otherwise -> Just "Package did not have default name",
PosTest
"no dependencies uses default stdlib"
$(mkRelDir "YamlNoDependencies")
$ \p b -> case p ^? packageDependencies . _head of
Just d ->
if
| d == defaultStdlibDep b -> Nothing
| otherwise -> Just "Package dependency is not the default standard library"
_ -> Just "The package has no dependencies",
PosTest
"empty dependencies does not use default stdlib"
$(mkRelDir "YamlEmptyDependencies")
$ \p _ ->
if
| null (p ^. packageDependencies) -> Nothing
| otherwise -> Just "Expected dependencies to be empty"
]

View File

@ -0,0 +1 @@
dependencies: 123

View File

@ -0,0 +1,10 @@
name: abc
dependencies:
- git:
url: repo
name: dep
ref: abc
- git:
url: repo
name: dep
ref: abc

View File

@ -0,0 +1,2 @@
name: abc
version: def

View File

@ -0,0 +1 @@
name: 123

View File

@ -0,0 +1,2 @@
name: abc
dependencies: []

View File

@ -0,0 +1 @@
name: abc

View File

@ -1056,6 +1056,8 @@ tests:
# compile project
juvix compile HelloWorld.juvix
stdout:
contains: ""
stderr:
contains: duplicate
exit-status: 1