diff --git a/app/Commands/Repl.hs b/app/Commands/Repl.hs index d333203ae6..af8ce4bbdc 100644 --- a/app/Commands/Repl.hs +++ b/app/Commands/Repl.hs @@ -147,7 +147,7 @@ loadDefaultPrelude = whenJustM defaultPreludeEntryPoint $ \e -> do . runM . evalInternet hasInternet . runFilesIO - . runError @Text + . runError @JuvixError . runReader e . runLogIO . runProcessIO diff --git a/src/Juvix/Compiler/Concrete/Translation/FromParsed/Analysis/PathResolver.hs b/src/Juvix/Compiler/Concrete/Translation/FromParsed/Analysis/PathResolver.hs index 7c1d452053..fd4dcee95a 100644 --- a/src/Juvix/Compiler/Concrete/Translation/FromParsed/Analysis/PathResolver.hs +++ b/src/Juvix/Compiler/Concrete/Translation/FromParsed/Analysis/PathResolver.hs @@ -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 diff --git a/src/Juvix/Compiler/Pipeline/Artifacts/PathResolver.hs b/src/Juvix/Compiler/Pipeline/Artifacts/PathResolver.hs index 2d0e658b9c..4e6c528925 100644 --- a/src/Juvix/Compiler/Pipeline/Artifacts/PathResolver.hs +++ b/src/Juvix/Compiler/Pipeline/Artifacts/PathResolver.hs @@ -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 diff --git a/src/Juvix/Compiler/Pipeline/Lockfile.hs b/src/Juvix/Compiler/Pipeline/Lockfile.hs index bc73a48e41..a0dea58b53 100644 --- a/src/Juvix/Compiler/Pipeline/Lockfile.hs +++ b/src/Juvix/Compiler/Pipeline/Lockfile.hs @@ -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 diff --git a/src/Juvix/Compiler/Pipeline/Package.hs b/src/Juvix/Compiler/Pipeline/Package.hs index 7ee3dc874d..63b2e62115 100644 --- a/src/Juvix/Compiler/Pipeline/Package.hs +++ b/src/Juvix/Compiler/Pipeline/Package.hs @@ -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 diff --git a/src/Juvix/Compiler/Pipeline/Package/Loader/Error.hs b/src/Juvix/Compiler/Pipeline/Package/Loader/Error.hs new file mode 100644 index 0000000000..b1e65c9180 --- /dev/null +++ b/src/Juvix/Compiler/Pipeline/Package/Loader/Error.hs @@ -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)) diff --git a/tests/smoke/Commands/compile-dependencies.smoke.yaml b/tests/smoke/Commands/compile-dependencies.smoke.yaml index 8c43d38472..18aff473c3 100644 --- a/tests/smoke/Commands/compile-dependencies.smoke.yaml +++ b/tests/smoke/Commands/compile-dependencies.smoke.yaml @@ -1056,6 +1056,8 @@ tests: # compile project juvix compile HelloWorld.juvix stdout: + contains: "" + stderr: contains: duplicate exit-status: 1