tala-core-0.1.0: src/Progmasoft/Tala/Core/Artifact.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Bounded, verified persistence for standalone Core artifacts.
Readers decode and semantically verify before returning a module; writers
verify before encoding so invalid compiler state never becomes a trusted file.
-}
module Progmasoft.Tala.Core.Artifact
( CoreArtifactError (..)
, readCoreArtifact
, readCoreArtifactWith
, writeCoreArtifact
, writeCoreArtifactWith
) where
import Control.Exception (IOException, try)
import Data.ByteString qualified as ByteString
import System.FilePath (takeExtension)
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.Verifier
import Progmasoft.Tala.Core.Wire
import Progmasoft.Tala.Diagnostic (diagnosticCode, diagnosticMessage)
-- | Path, wire, semantic, or filesystem failure while handling a Core file.
data CoreArtifactError
= -- | Artifact path does not use the required @.core@ extension.
InvalidCoreArtifactPath FilePath
| -- | The byte document failed wire decoding or encoding.
CoreArtifactWireError CoreWireError
| -- | Decoded Core failed semantic verification.
CoreArtifactVerificationError [String]
| -- | Filesystem operation failed at this path.
CoreArtifactIOError FilePath String
deriving (Eq, Ord, Read, Show)
-- | Read, decode, and verify a Core artifact using default wire ceilings.
readCoreArtifact :: FilePath -> IO (Either CoreArtifactError CoreModule)
readCoreArtifact = readCoreArtifactWith defaultCoreWireLimits
-- | Read and verify a Core artifact under caller-selected resource limits.
readCoreArtifactWith :: CoreWireLimits -> FilePath -> IO (Either CoreArtifactError CoreModule)
readCoreArtifactWith limits path
| takeExtension path /= ".core" = pure (Left (InvalidCoreArtifactPath path))
| otherwise = do
loaded <- try (ByteString.readFile path) :: IO (Either IOException ByteString.ByteString)
pure $ case loaded of
Left issue -> Left (CoreArtifactIOError path (show issue))
Right bytes -> case decodeCore limits (ByteString.unpack bytes) of
Left issue -> Left (CoreArtifactWireError issue)
Right moduleValue -> verify moduleValue
-- | Verify and write a Core module using default wire ceilings.
writeCoreArtifact :: FilePath -> CoreModule -> IO (Either CoreArtifactError ())
writeCoreArtifact = writeCoreArtifactWith defaultCoreWireLimits
-- | Verify and write a Core module under caller-selected resource limits.
writeCoreArtifactWith :: CoreWireLimits -> FilePath -> CoreModule -> IO (Either CoreArtifactError ())
writeCoreArtifactWith limits path moduleValue
| takeExtension path /= ".core" = pure (Left (InvalidCoreArtifactPath path))
| otherwise = case verify moduleValue >>= encode of
Left issue -> pure (Left issue)
Right bytes -> do
written <- try (ByteString.writeFile path (ByteString.pack bytes)) :: IO (Either IOException ())
pure $ either (Left . CoreArtifactIOError path . show) (const (Right ())) written
where
encode value = either (Left . CoreArtifactWireError) Right (encodeCore limits value)
verify :: CoreModule -> Either CoreArtifactError CoreModule
verify moduleValue = case verifyCore moduleValue of
Left problems -> Left (CoreArtifactVerificationError (map diagnosticText problems))
Right verified -> Right verified
where
diagnosticText diagnostic = diagnosticCode diagnostic ++ ": " ++ diagnosticMessage diagnostic