diff --git a/plutus-core/changelog.d/20240618_023306_effectfully_replace_EvaluationResult_with_BuiltinResult.md b/plutus-core/changelog.d/20240618_023306_effectfully_replace_EvaluationResult_with_BuiltinResult.md new file mode 100644 index 00000000000..2ba2a33de8f --- /dev/null +++ b/plutus-core/changelog.d/20240618_023306_effectfully_replace_EvaluationResult_with_BuiltinResult.md @@ -0,0 +1,3 @@ +### Changed + +- Forbade using `EvaluationResult` in the builtins code in favor of `BuiltinResult` in #5926, so that builtins throw errors with more helpful messages. diff --git a/plutus-core/cost-model/budgeting-bench/Benchmarks/Nops.hs b/plutus-core/cost-model/budgeting-bench/Benchmarks/Nops.hs index 213960ccd23..92c57db4528 100644 --- a/plutus-core/cost-model/budgeting-bench/Benchmarks/Nops.hs +++ b/plutus-core/cost-model/budgeting-bench/Benchmarks/Nops.hs @@ -22,6 +22,7 @@ import PlutusCore.Evaluation.Machine.BuiltinCostModel hiding (BuiltinCostModel) import PlutusCore.Evaluation.Machine.ExBudgetingDefaults import PlutusCore.Evaluation.Machine.ExMemoryUsage (ExMemoryUsage) import PlutusCore.Evaluation.Machine.MachineParameters +import PlutusCore.Evaluation.Result (evaluationFailure) import PlutusCore.Pretty import PlutusPrelude import UntypedPlutusCore.Evaluation.Machine.Cek @@ -132,12 +133,12 @@ nopCostParameters = infixr >: (>:) :: uni ~ DefaultUni => SomeConstant uni Integer - -> EvaluationResult Integer - -> EvaluationResult Integer + -> BuiltinResult Integer + -> BuiltinResult Integer n >: k = case n of SomeConstant (Some (ValueOf DefaultUniInteger _)) -> k - _ -> EvaluationFailure + _ -> evaluationFailure {- | The meanings of the builtins. Each one takes a number of arguments and returns a result without doing any other work. A builtin can process its @@ -225,27 +226,27 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni NopFun where -- Integers unlifted via SomeConstant toBuiltinMeaning _semvar Nop1c = makeBuiltinMeaning - (\c1 -> c1 >: EvaluationSuccess 11) + (\c1 -> c1 >: BuiltinSuccess 11) (runCostingFunOneArgument . paramNop1) toBuiltinMeaning _semvar Nop2c = makeBuiltinMeaning - (\c1 c2 -> c1 >: c2 >: EvaluationSuccess 22) + (\c1 c2 -> c1 >: c2 >: BuiltinSuccess 22) (runCostingFunTwoArguments . paramNop2) toBuiltinMeaning _semvar Nop3c = makeBuiltinMeaning - (\c1 c2 c3 -> c1 >: c2 >: c3 >: EvaluationSuccess 33) + (\c1 c2 c3 -> c1 >: c2 >: c3 >: BuiltinSuccess 33) (runCostingFunThreeArguments . paramNop3) toBuiltinMeaning _semvar Nop4c = makeBuiltinMeaning - (\c1 c2 c3 c4 -> c1 >: c2 >: c3 >: c4 >: EvaluationSuccess 44) + (\c1 c2 c3 c4 -> c1 >: c2 >: c3 >: c4 >: BuiltinSuccess 44) (runCostingFunFourArguments . paramNop4) toBuiltinMeaning _semvar Nop5c = makeBuiltinMeaning - (\c1 c2 c3 c4 c5 -> c1 >: c2 >: c3 >: c4 >: c5 >: EvaluationSuccess 55) + (\c1 c2 c3 c4 c5 -> c1 >: c2 >: c3 >: c4 >: c5 >: BuiltinSuccess 55) (runCostingFunFiveArguments . paramNop5) toBuiltinMeaning _semvar Nop6c = makeBuiltinMeaning - (\c1 c2 c3 c4 c5 c6 -> c1 >: c2 >: c3 >: c4 >: c5 >: c6 >: EvaluationSuccess 66) + (\c1 c2 c3 c4 c5 c6 -> c1 >: c2 >: c3 >: c4 >: c5 >: c6 >: BuiltinSuccess 66) (runCostingFunSixArguments . paramNop6) -- Opaque Integers toBuiltinMeaning _semvar Nop1o = diff --git a/plutus-core/plutus-core/examples/PlutusCore/Examples/Builtins.hs b/plutus-core/plutus-core/examples/PlutusCore/Examples/Builtins.hs index 96755b93c7a..0207daa7a43 100644 --- a/plutus-core/plutus-core/examples/PlutusCore/Examples/Builtins.hs +++ b/plutus-core/plutus-core/examples/PlutusCore/Examples/Builtins.hs @@ -24,6 +24,7 @@ import PlutusCore.Data import PlutusCore.Evaluation.Machine.BuiltinCostModel import PlutusCore.Evaluation.Machine.ExBudget import PlutusCore.Evaluation.Machine.ExBudgetStream +import PlutusCore.Evaluation.Result (evaluationFailure) import PlutusCore.Pretty import PlutusCore.StdLib.Data.ScottList qualified as Plc @@ -277,31 +278,31 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where idAssumeCheckBoolPlc whatever where - idAssumeCheckBoolPlc :: Opaque val Bool -> EvaluationResult Bool + idAssumeCheckBoolPlc :: Opaque val Bool -> BuiltinResult Bool idAssumeCheckBoolPlc val = case asConstant val of - Right (Some (ValueOf DefaultUniBool b)) -> EvaluationSuccess b - _ -> EvaluationFailure + Right (Some (ValueOf DefaultUniBool b)) -> pure b + _ -> evaluationFailure toBuiltinMeaning _semvar IdSomeConstantBool = makeBuiltinMeaning idSomeConstantBoolPlc whatever where - idSomeConstantBoolPlc :: SomeConstant uni Bool -> EvaluationResult Bool + idSomeConstantBoolPlc :: SomeConstant uni Bool -> BuiltinResult Bool idSomeConstantBoolPlc = \case - SomeConstant (Some (ValueOf DefaultUniBool b)) -> EvaluationSuccess b - _ -> EvaluationFailure + SomeConstant (Some (ValueOf DefaultUniBool b)) -> pure b + _ -> evaluationFailure toBuiltinMeaning _semvar IdIntegerAsBool = makeBuiltinMeaning idIntegerAsBool whatever where - idIntegerAsBool :: SomeConstant uni Integer -> EvaluationResult (SomeConstant uni Integer) + idIntegerAsBool :: SomeConstant uni Integer -> BuiltinResult (SomeConstant uni Integer) idIntegerAsBool = \case - con@(SomeConstant (Some (ValueOf DefaultUniBool _))) -> EvaluationSuccess con - _ -> EvaluationFailure + con@(SomeConstant (Some (ValueOf DefaultUniBool _))) -> pure con + _ -> evaluationFailure toBuiltinMeaning _semvar IdFInteger = makeBuiltinMeaning @@ -380,8 +381,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where whatever where unsafeCoerceElPlc - :: SomeConstant DefaultUni [a] - -> EvaluationResult (SomeConstant DefaultUni [b]) + :: SomeConstant DefaultUni [a] -> BuiltinResult (SomeConstant DefaultUni [b]) unsafeCoerceElPlc (SomeConstant (Some (ValueOf uniList xs))) = do DefaultUniList _ <- pure uniList pure $ fromValueOf uniList xs @@ -398,7 +398,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where toBuiltinMeaning _semvar ErrorPrime = makeBuiltinMeaning - EvaluationFailure + (evaluationFailure :: forall a. BuiltinResult a) whatever toBuiltinMeaning _semvar Comma = @@ -422,7 +422,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where :: SomeConstant uni a -> SomeConstant uni b -> SomeConstant uni (a, b) - -> EvaluationResult (SomeConstant uni (a, b)) + -> BuiltinResult (SomeConstant uni (a, b)) biconstPairPlc (SomeConstant (Some (ValueOf uniA x))) (SomeConstant (Some (ValueOf uniB y))) @@ -439,7 +439,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where where swapPlc :: SomeConstant uni (a, b) - -> EvaluationResult (SomeConstant uni (b, a)) + -> BuiltinResult (SomeConstant uni (b, a)) swapPlc (SomeConstant (Some (ValueOf uniPairAB p))) = do DefaultUniPair uniA uniB <- pure uniPairAB pure $ fromValueOf (DefaultUniPair uniB uniA) (snd p, fst p) @@ -452,7 +452,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where -- The type reads as @[(a, Bool)] -> [(Bool, a)]@. swapElsPlc :: SomeConstant uni [SomeConstant uni (a, Bool)] - -> EvaluationResult (SomeConstant uni [SomeConstant uni (Bool, a)]) + -> BuiltinResult (SomeConstant uni [SomeConstant uni (Bool, a)]) swapElsPlc (SomeConstant (Some (ValueOf uniList xs))) = do DefaultUniList (DefaultUniPair uniA DefaultUniBool) <- pure uniList let uniList' = DefaultUniList $ DefaultUniPair DefaultUniBool uniA @@ -462,10 +462,10 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni ExtensionFun where -- See Note [Builtin semantics variants] toBuiltinMeaning semvar ExtensionVersion = makeBuiltinMeaning - @(() -> EvaluationResult Integer) - (\(_ :: ()) -> EvaluationSuccess $ case semvar of - ExtensionFunSemanticsVariantX -> 0 - ExtensionFunSemanticsVariantY -> 1) + @(() -> Integer) + (\_ -> case semvar of + ExtensionFunSemanticsVariantX -> 0 + ExtensionFunSemanticsVariantY -> 1) whatever -- We want to know if the CEK machine releases individual budgets after accounting for them and diff --git a/plutus-core/plutus-core/src/PlutusCore/Builtin/KnownType.hs b/plutus-core/plutus-core/src/PlutusCore/Builtin/KnownType.hs index 20ac23f9cf8..3c4b7a79cf9 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Builtin/KnownType.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Builtin/KnownType.hs @@ -248,9 +248,8 @@ typeMismatchError uniExp uniAct = , "expected: " ++ displayBy botRenderContext (SomeTypeIn uniExp) , "; actual: " ++ displayBy botRenderContext (SomeTypeIn uniAct) ] --- Just for tidier Core to get generated, we don't care about performance here, since it's just a --- failure message and evaluation is about to be shut anyway. -{-# NOINLINE typeMismatchError #-} +-- See Note [INLINE and OPAQUE on error-related definitions]. +{-# OPAQUE typeMismatchError #-} -- Normally it's a good idea for an exported abstraction not to be a type synonym, since a @newtype@ -- is cheap, looks good in error messages and clearly emphasize an abstraction barrier. However we @@ -322,11 +321,6 @@ readKnownSelf readKnownSelf val = fromRightM (throwBuiltinErrorWithCause val) $ readKnown val {-# INLINE readKnownSelf #-} -instance MakeKnownIn uni val a => MakeKnownIn uni val (EvaluationResult a) where - makeKnown EvaluationFailure = evaluationFailure - makeKnown (EvaluationSuccess x) = makeKnown x - {-# INLINE makeKnown #-} - instance MakeKnownIn uni val a => MakeKnownIn uni val (BuiltinResult a) where makeKnown res = res >>= makeKnown {-# INLINE makeKnown #-} @@ -338,24 +332,38 @@ instance MakeKnownIn uni val a => MakeKnownIn uni val (BuiltinResult a) where -- I.e. it would essentially allow us to catch errors and handle them in a programmable way. -- We forbid this, because it complicates code and isn't supported by evaluation engines anyway. instance - ( TypeError ('Text "‘EvaluationResult’ cannot appear in the type of an argument") + ( TypeError ('Text "‘BuiltinResult’ cannot appear in the type of an argument") + , uni ~ UniOf val + ) => ReadKnownIn uni val (BuiltinResult a) where + readKnown _ = throwUnderTypeError + {-# INLINE readKnown #-} + +instance + ( TypeError ('Text "Use ‘BuiltinResult’ instead of ‘EvaluationResult’") + , uni ~ UniOf val + ) => MakeKnownIn uni val (EvaluationResult a) where + makeKnown _ = throwUnderTypeError + {-# INLINE makeKnown #-} + +instance + ( TypeError ('Text "Use ‘BuiltinResult’ instead of ‘EvaluationResult’") , uni ~ UniOf val ) => ReadKnownIn uni val (EvaluationResult a) where - readKnown _ = throwing _StructuralUnliftingError "Panic: 'TypeError' was bypassed" - -- Just for 'readKnown' not to appear in the generated Core. + readKnown _ = throwUnderTypeError {-# INLINE readKnown #-} -instance MakeKnownIn uni val a => MakeKnownIn uni val (Emitter a) where - makeKnown a = case runEmitter a of - (x, logs) -> withLogs logs $ makeKnown x +instance + ( TypeError ('Text "Use ‘BuiltinResult’ instead of ‘Emitter’") + , uni ~ UniOf val + ) => MakeKnownIn uni val (Emitter a) where + makeKnown _ = throwUnderTypeError {-# INLINE makeKnown #-} instance - ( TypeError ('Text "‘Emitter’ cannot appear in the type of an argument") + ( TypeError ('Text "Use ‘BuiltinResult’ instead of ‘Emitter’") , uni ~ UniOf val ) => ReadKnownIn uni val (Emitter a) where - readKnown _ = throwing _StructuralUnliftingError "Panic: 'TypeError' was bypassed" - -- Just for 'readKnown' not to appear in the generated Core. + readKnown _ = throwUnderTypeError {-# INLINE readKnown #-} instance HasConstantIn uni val => MakeKnownIn uni val (SomeConstant uni rep) where diff --git a/plutus-core/plutus-core/src/PlutusCore/Builtin/Meaning.hs b/plutus-core/plutus-core/src/PlutusCore/Builtin/Meaning.hs index 2c04f75ee56..f7260d203d7 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Builtin/Meaning.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Builtin/Meaning.hs @@ -30,7 +30,6 @@ import PlutusCore.Evaluation.Machine.ExBudgetStream import PlutusCore.Evaluation.Machine.ExMemoryUsage import PlutusCore.Name.Unique -import Control.Monad.Except (throwError) import Data.Array import Data.Kind qualified as GHC import Data.Proxy @@ -244,7 +243,7 @@ instance (Typeable res, KnownTypeAst TyName (UniOf val) res, MakeKnown val res) -- either a budgeting failure or a budgeting success with a cost and a 'BuiltinResult' -- computation inside, but that would slow things down a bit and the current strategy is -- reasonable enough. - (BuiltinCostedResult (ExBudgetLast mempty) . throwError) + builtinRuntimeFailure (\(x, cost) -> BuiltinCostedResult cost $ makeKnown x) {-# INLINE toMonoF #-} diff --git a/plutus-core/plutus-core/src/PlutusCore/Builtin/Result.hs b/plutus-core/plutus-core/src/PlutusCore/Builtin/Result.hs index 44a44e1fa34..b1685d6b5fb 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Builtin/Result.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Builtin/Result.hs @@ -1,9 +1,11 @@ +-- editorconfig-checker-disable-file {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} module PlutusCore.Builtin.Result @@ -21,6 +23,7 @@ module PlutusCore.Builtin.Result , _StructuralUnliftingError , _OperationalUnliftingError , throwNotAConstant + , throwUnderTypeError , withLogs , throwing , throwing_ @@ -39,13 +42,14 @@ import Data.Bitraversable import Data.DList (DList) import Data.String (IsString) import Data.Text (Text) +import Data.Text qualified as Text import Prettyprinter -- | The error message part of an 'UnliftingEvaluationError'. newtype UnliftingError = MkUnliftingError { unUnliftingError :: Text } deriving stock (Show, Eq) - deriving newtype (IsString, Semigroup, NFData) + deriving newtype (IsString, Semigroup, Monoid, NFData) -- | When unlifting of a PLC term into a Haskell value fails, this error is thrown. newtype UnliftingEvaluationError = MkUnliftingEvaluationError @@ -55,7 +59,7 @@ newtype UnliftingEvaluationError = MkUnliftingEvaluationError -- | The type of errors that 'readKnown' and 'makeKnown' can return. data BuiltinError - = BuiltinUnliftingEvaluationError !UnliftingEvaluationError + = BuiltinUnliftingEvaluationError UnliftingEvaluationError | BuiltinEvaluationFailure deriving stock (Show, Eq) @@ -143,6 +147,10 @@ instance MonadEmitter BuiltinResult where emit txt = BuiltinSuccessWithLogs (pure txt) () {-# INLINE emit #-} +instance MonadFail BuiltinResult where + fail err = BuiltinFailure (pure $ Text.pack err) BuiltinEvaluationFailure + {-# INLINE fail #-} + instance Pretty UnliftingError where pretty (MkUnliftingError err) = fold [ "Could not unlift a value:", hardline @@ -155,6 +163,21 @@ instance Pretty BuiltinError where pretty (BuiltinUnliftingEvaluationError err) = "Builtin evaluation failure:" <+> pretty err pretty BuiltinEvaluationFailure = "Builtin evaluation failure" +{- Note [INLINE and OPAQUE on error-related definitions] +We mark error-related definitions such as prisms like '_StructuralUnliftingError' and regular +functions like 'throwNotAConstant' with @INLINE@, because this produces significantly less cluttered +GHC Core. Not doing so results in 20+% larger Core for builtins. + +However in a few specific cases we use @OPAQUE@ instead to get tighter Core. @OPAQUE@ is the same as +@NOINLINE@ except the former _actually_ prevents GHC from inlining the definition unlike the latter. +See this for details: https://github.com/ghc-proposals/ghc-proposals/blob/5577fd008924de8d89cfa9855fa454512e7dcc75/proposals/0415-opaque-pragma.rst + +It's hard to predict where @OPAQUE@ instead of @INLINE@ will help to make GHC Core tidier, so it's +mostly just looking into the Core and seeing where there's obvious duplication that can be removed. +Such cases tend to be functions returning a value of a concrete error type (as opposed to a type +variable). +-} + -- See Note [Ignoring context in OperationalEvaluationError]. -- | Construct a prism focusing on the @*EvaluationFailure@ part of @err@ by taking -- that @*EvaluationFailure@ and @@ -181,6 +204,10 @@ throwNotAConstant :: MonadError BuiltinError m => m void throwNotAConstant = throwing _StructuralUnliftingError "Not a constant" {-# INLINE throwNotAConstant #-} +throwUnderTypeError :: MonadError BuiltinError m => m void +throwUnderTypeError = throwing _StructuralUnliftingError "Panic: 'TypeError' was bypassed" +{-# INLINE throwUnderTypeError #-} + -- | Prepend logs to a 'BuiltinResult' computation. withLogs :: DList Text -> BuiltinResult a -> BuiltinResult a withLogs logs1 = \case @@ -242,6 +269,7 @@ instance MonadError BuiltinError BuiltinResult where (OperationalEvaluationError (MkUnliftingError operationalErr))) -> pure operationalErr _ -> mempty + {-# INLINE throwError #-} -- Throwing logs out is lame, but embedding them into the error would be weird, since that -- would change the error. Not that any of that matters, we only implement this because it's a diff --git a/plutus-core/plutus-core/src/PlutusCore/Builtin/Runtime.hs b/plutus-core/plutus-core/src/PlutusCore/Builtin/Runtime.hs index a77378a4218..805962c2c62 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Builtin/Runtime.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Builtin/Runtime.hs @@ -10,6 +10,7 @@ import PlutusCore.Builtin.KnownType import PlutusCore.Evaluation.Machine.ExBudgetStream import Control.DeepSeq +import Control.Monad.Except (throwError) import NoThunks.Class -- | A 'BuiltinRuntime' represents a possibly partial builtin application, including an empty @@ -78,6 +79,11 @@ instance (Bounded fun, Enum fun) => NoThunks (BuiltinsRuntime fun val) where wNoThunks ctx (BuiltinsRuntime env) = allNoThunks $ map (wNoThunks ctx . env) enumerate showTypeOf = const "PlutusCore.Builtin.Runtime.BuiltinsRuntime" +builtinRuntimeFailure :: BuiltinError -> BuiltinRuntime val +builtinRuntimeFailure = BuiltinCostedResult (ExBudgetLast mempty) . throwError +-- See Note [INLINE and OPAQUE on error-related definitions]. +{-# OPAQUE builtinRuntimeFailure #-} + -- | Look up the runtime info of a built-in function during evaluation. lookupBuiltin :: fun -> BuiltinsRuntime fun val -> BuiltinRuntime val lookupBuiltin fun (BuiltinsRuntime env) = env fun diff --git a/plutus-core/plutus-core/src/PlutusCore/Crypto/Utils.hs b/plutus-core/plutus-core/src/PlutusCore/Crypto/Utils.hs index a67300c0310..bb80ce83dcb 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Crypto/Utils.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Crypto/Utils.hs @@ -12,11 +12,6 @@ import Data.Kind (Type) import Data.Text (Text) import Text.Printf (printf) --- TODO: Something like 'failWithMessage x y *> foo' should really fail with --- 'EvaluationFailure' without evaluating 'foo', but currently it will. This --- requires a fix to how Emitter and EvaluationResult work, and since we don't --- expect 'failWithMessage' to be used this way, we note this for future --- reference only for when such fixes are made. failWithMessage :: forall (a :: Type). Text -> Text -> BuiltinResult a failWithMessage location reason = do emit $ location <> ": " <> reason diff --git a/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs b/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs index ecc6bc4f5f0..e4a51c5e2d0 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs @@ -1,4 +1,5 @@ -- editorconfig-checker-disable-file +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE FlexibleInstances #-} @@ -22,7 +23,6 @@ import PlutusCore.Evaluation.Machine.BuiltinCostModel import PlutusCore.Evaluation.Machine.ExBudgetStream (ExBudgetStream) import PlutusCore.Evaluation.Machine.ExMemoryUsage (ExMemoryUsage, LiteralByteSize (..), memoryUsage, singletonRose) -import PlutusCore.Evaluation.Result (EvaluationResult (..)) import PlutusCore.Pretty (PrettyConfigPlc) import PlutusCore.Bitwise qualified as Bitwise @@ -34,10 +34,11 @@ import PlutusCore.Crypto.Hash qualified as Hash import PlutusCore.Crypto.Secp256k1 (verifyEcdsaSecp256k1Signature, verifySchnorrSecp256k1Signature) import Codec.Serialise (serialise) +import Control.Monad (unless) import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BSL import Data.Ix (Ix) -import Data.Text (Text, pack) +import Data.Text (Text) import Data.Text.Encoding (decodeUtf8', encodeUtf8) import Flat hiding (from, to) import Flat.Decoder (Get, dBEBits8) @@ -178,22 +179,36 @@ instance Pretty DefaultFun where instance ExMemoryUsage DefaultFun where memoryUsage _ = singletonRose 1 + {-# INLINE memoryUsage #-} --- | Turn a function into another function that returns 'EvaluationFailure' when --- its second argument is 0 or calls the original function otherwise and wraps --- the result in 'EvaluationSuccess'. Useful for correctly handling `div`, +-- | Turn a function into another function that 'fail's when its second argument is @0@ or calls the +-- original function otherwise and wraps the result in 'pure'. Useful for correctly handling `div`, -- `mod`, etc. nonZeroSecondArg - :: (Integer -> Integer -> Integer) -> Integer -> Integer -> EvaluationResult Integer -nonZeroSecondArg _ _ 0 = EvaluationFailure -nonZeroSecondArg f x y = EvaluationSuccess $ f x y - --- | Turn a function returning 'Either' into another function that emits an --- error message and returns 'EvaluationFailure' in the 'Left' case and wraps --- the result in 'EvaluationSuccess' in the 'Right' case. -eitherToEmitter :: Show e => Either e r -> Emitter (EvaluationResult r) -eitherToEmitter (Left e) = (emit . pack . show $ e) >> pure EvaluationFailure -eitherToEmitter (Right r) = pure . pure $ r + :: (Integer -> Integer -> Integer) -> Integer -> Integer -> BuiltinResult Integer +-- If we match against @IS 0#@ instead of @0@, GHC will generate tidier Core for some reason. It +-- probably doesn't really matter performance-wise, but would be easier to read. We don't do it out +-- of paranoia and because it requires importing the 'IS' constructor, which is in different +-- packages depending on the GHC version, so requires a bunch of irritating CPP. +-- +-- We could also replace 'div' with 'integerDiv' (and do the same for other division builtins) at +-- the call site of this function in order to avoid double matching against @0@, but that also +-- requires CPP. Perhaps we can afford one additional pattern match for division builtins for the +-- time being, since those aren't particularly fast anyway. +-- +-- The bang is to communicate to GHC that the function is strict in both the arguments just in case +-- it'd want to allocate a thunk for the first argument otherwise. +nonZeroSecondArg _ !_ 0 = + -- See Note [Operational vs structural errors within builtins]. + fail "Cannot divide by zero" +nonZeroSecondArg f x y = pure $ f x y +{-# INLINE nonZeroSecondArg #-} + +-- | Turn a function returning 'Either' into another function that 'fail's in the 'Left' case and +-- wraps the result in 'pure' in the 'Right' case. +eitherToBuiltinResult :: Show e => Either e r -> BuiltinResult r +eitherToBuiltinResult = either (fail . show) pure +{-# INLINE eitherToBuiltinResult #-} {- Note [Constants vs built-in functions] A constant is any value of a built-in type. For example, 'Integer' is a built-in type, so anything @@ -259,7 +274,7 @@ it within the @ToBuiltinMeaning uni DefaultFun@ instance. The general pattern is Here's a specific example: - toBuiltinMeaning _semvar AddInteger = + toBuiltinMeaning _ AddInteger = let addIntegerDenotation :: Integer -> Integer -> Integer addIntegerDenotation = (+) {-# INLINE addIntegerDenotation #-} @@ -383,29 +398,29 @@ There's a number of ways a builtin can fail: - as we've just seen a type conversion can fail due to an unsuccessful bounds check - if the builtin expects, say, a 'Text' argument, but gets fed an 'Integer' argument - if the builtin expects any constant, but gets fed a non-constant -- if its denotation runs in the 'EvaluationResult' and an 'EvaluationFailure' gets returned +- if its denotation runs in the 'BuiltinResult' monad and an 'evaluationFailure' gets returned Most of these are not a concern to the user defining a built-in function (conversions are handled within the builtin application machinery, type mismatches are on the type checker and the person -writing the program etc), however explicitly returning 'EvaluationFailure' from a builtin is +writing the program etc), however explicitly returning 'evaluationFailure' from a builtin is something that happens commonly. One simple example is a monomorphic function matching on a certain constructor and failing in all other cases: toBuiltinMeaning _ UnIData = - let unIDataDenotation :: Data -> EvaluationResult Integer + let unIDataDenotation :: Data -> BuiltinResult Integer unIDataDenotation = \case - I i -> EvaluationSuccess i - _ -> EvaluationFailure + I i -> pure i + _ -> evaluationFailure {-# INLINE unIDataDenotation #-} in makeBuiltinMeaning unIDataDenotation -The inferred type of the denotation is +The type of the denotation is - Data -> EvaluationResult Integer + Data -> BuiltinResult Integer and the Plutus type of the builtin is @@ -413,32 +428,31 @@ and the Plutus type of the builtin is because the error effect is implicit in Plutus. -Returning @EvaluationResult a@ for a type variable @a@ is also fine, i.e. it doesn't matter whether +Returning @BuiltinResult a@ for a type variable @a@ is also fine, i.e. it doesn't matter whether the denotation is monomorphic or polymorphic w.r.t. failing. But note that - 'EvaluationResult' MUST BE EXPLICITLY USED FOR ANY FAILING BUILTIN AND THROWING AN EXCEPTION + 'BuiltinResult' MUST BE EXPLICITLY USED FOR ANY FAILING BUILTIN AND THROWING AN EXCEPTION VIA 'error' OR 'throw' OR ELSE IS NOT ALLOWED AND CAN BE A HUGE VULNERABILITY. MAKE SURE THAT NONE OF THE FUNCTIONS THAT YOU USE TO DEFINE A BUILTIN THROW EXCEPTIONS -An argument of a builtin can't have 'EvaluationResult' in its type -- only the result. +An argument of a builtin can't have 'BuiltinResult' in its type -- only the result. -5. A builtin can emit log messages. For that it needs to run in the 'Emitter' monad. The ergonomics -are the same as with 'EvaluationResult': 'Emitter' can't appear in the type of an argument and -polymorphism is fine. For example: +5. A builtin can emit log messages. For that its denotation needs to run in the 'BuiltinResult' as +in case of failing. The ergonomics are the same. For example: toBuiltinMeaning _ Trace = - let traceDenotation :: Text -> a -> Emitter a + let traceDenotation :: Text -> a -> BuiltinResult a traceDenotation text a = a <$ emit text {-# INLINE traceDenotation #-} in makeBuiltinMeaning traceDenotation -The inferred type of the denotation is +The type of the denotation is - forall a. Text -> a -> Emitter a + forall a. Text -> a -> Builtin a and the Plutus type of the builtin is @@ -447,10 +461,6 @@ and the Plutus type of the builtin is because just like with the error effect, whether a function logs anything or not is not reflected in its type. -'makeBuiltinMeaning' allows one to nest 'EvaluationResult' inside of 'Emitter' and vice versa, -but as always nesting monads inside of each other without using monad transformers doesn't have good -ergonomics, since computations of such a type can't be chained with a simple @(>>=)@. - This concludes the list of simple cases. Before we jump to the hard ones, we need to talk about how polymorphism gets elaborated, so read Note [Elaboration of polymorphism] next. -} @@ -466,7 +476,7 @@ In Note [How to add a built-in function: simple cases] we defined the following ifThenElseDenotation -whose inferred Haskell type is +whose Haskell type is forall a. Bool -> a -> a -> a @@ -565,11 +575,11 @@ It's of course allowed to have multiple type variables, e.g. in the following sn constDenotation -the Haskell type of 'const' gets inferred as +the Haskell type of 'const' is forall a b. a -> b -> a -and the elaboration machinery turns that into +which the elaboration machinery turns into Opaque val Var0 -> Opaque val Var1 -> Opaque val Var0 @@ -584,28 +594,27 @@ the elaboration machinery wouldn't make a fuss about that. As a final simple example, consider toBuiltinMeaning _ Trace = - let traceDenotation :: Text -> a -> Emitter a + let traceDenotation :: Text -> a -> BuiltinResult a traceDenotation text a = a <$ emit text {-# INLINE traceDenotation #-} in makeBuiltinMeaning traceDenotation -from [How to add a built-in function: simple cases]. The inferred type of the denotation is +from [How to add a built-in function: simple cases]. The type of the denotation is - forall a. Text -> a -> Emitter a + forall a. Text -> a -> BuiltinResult a which elaborates to - Text -> Opaque val Var0 -> Emitter (Opaque val Var0) + Text -> Opaque val Var0 -> BuiltinResult (Opaque val Var0) -Elaboration machinery is able to look under 'Emitter' and 'EvaluationResult' even if there's a type -variable inside that does not appear anywhere else in the type signature, for example the inferred -type of the denotation in +Elaboration machinery is able to look under 'BuiltinResult' even if there's a type variable inside +that does not appear anywhere else in the type signature, for example the type of the denotation in toBuiltinMeaning _ ErrorPrime = - let errorPrimeDenotation :: EvaluationResult a - errorPrimeDenotation = EvaluationFailure + let errorPrimeDenotation :: BuiltinResult a + errorPrimeDenotation = evaluationFailure {-# INLINE errorPrimeDenotation #-} in makeBuiltinMeaning errorPrimeDenotation @@ -613,11 +622,11 @@ type of the denotation in is - forall a. EvaluationResult a + forall a. BuiltinResult a which gets elaborated to - EvaluationResult (Opaque val Var0) + BuiltinResult (Opaque val Var0) from which the final Plutus type of the builtin is computed: @@ -671,10 +680,10 @@ reason, wanted to have 'Opaque' in the type signature of the denotation, but sti argument as a 'Bool', we could do that: toBuiltinMeaning _ IdAssumeCheckBool = - let idAssumeCheckBoolDenotation :: Opaque val Bool -> EvaluationResult Bool + let idAssumeCheckBoolDenotation :: Opaque val Bool -> BuiltinResult Bool idAssumeCheckBoolDenotation val = asConstant val of - Right (Some (ValueOf DefaultUniBool b)) -> EvaluationSuccess b - _ -> EvaluationFailure + Right (Some (ValueOf DefaultUniBool b)) -> pure b + _ -> evaluationFailure {-# INLINE idAssumeCheckBoolDenotation #-} in makeBuiltinMeaning idAssumeCheckBoolDenotation @@ -682,7 +691,7 @@ argument as a 'Bool', we could do that: Here in the denotation we unlift the given value as a constant, check that its type tag is 'DefaultUniBool' and return the unlifted 'Bool'. If any of that fails, we return an explicit -'EvaluationFailure'. +'evaluationFailure'. This achieves almost the same as 'IdBool', which keeps all the bookkeeping behind the scenes, but there is a minor difference: in case of error its message is ignored. It would be easy to allow for @@ -708,10 +717,10 @@ wrapper around a constant. 'SomeConstant' allows one to automatically unlift an built-in function as a constant with all 'asConstant' business kept behind the scenes, for example: toBuiltinMeaning _ IdSomeConstantBool = - let idSomeConstantBoolDenotation :: SomeConstant uni Bool -> EvaluationResult Bool + let idSomeConstantBoolDenotation :: SomeConstant uni Bool -> BuiltinResult Bool idSomeConstantBoolDenotation = \case - SomeConstant (Some (ValueOf DefaultUniBool b)) -> EvaluationSuccess b - _ -> EvaluationFailure + SomeConstant (Some (ValueOf DefaultUniBool b)) -> pure b + _ -> evaluationFailure {-# INLINE idSomeConstantBoolDenotation #-} in makeBuiltinMeaning idSomeConstantBoolDenotation @@ -732,26 +741,24 @@ However it's not always possible to use automatic unlifting, see next. nullListDenotation -we'll get an error, saying that a polymorphic built-in type can't be applied to -a type variable. It's not impossible to make it work, see Note [Unlifting a -term as a value of a built-in type], but not in the general case, plus it has to -be very inefficient. +we'll get an error, saying that a polymorphic built-in type can't be applied to a type variable. +It's not impossible to make it work, see Note [Unlifting a term as a value of a built-in type], but +not in the general case, plus it has to be very inefficient. Instead we have to use 'SomeConstant' to automatically unlift the argument as a constant and then check that the value inside of it is a list (by matching on the type tag): toBuiltinMeaning _ NullList = - let nullListDenotation :: SomeConstant uni [a] -> EvaluationResult Bool + let nullListDenotation :: SomeConstant uni [a] -> BuiltinResult Bool nullListDenotation (SomeConstant (Some (ValueOf uniListA xs))) = do - DefaultUniList _ <- pure uniListA - pure $ null xs + case uniListA of + DefaultUniList _ -> pure $ null xs + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE nullListDenotation #-} in makeBuiltinMeaning nullListDenotation -('EvaluationResult' has a 'MonadFail' instance allowing us to use the @ <- pure @ idiom) - As before, we have to match on the type tag, because there's no relation between @rep@ from @SomeConstant uni rep@ and the constant that the built-in function actually receives at runtime (someone could generate Untyped Plutus Core directly and apply 'nullPlc' to an 'Integer' or @@ -761,10 +768,13 @@ in any way. Here's a similar built-in function: toBuiltinMeaning _ FstPair = - let fstPairDenotation :: SomeConstant uni (a, b) -> EvaluationResult (Opaque val a) + let fstPairDenotation :: SomeConstant uni (a, b) -> BuiltinResult (Opaque val a) fstPairDenotation (SomeConstant (Some (ValueOf uniPairAB xy))) = do - DefaultUniPair uniA _ <- pure uniPairAB -- [1] - pure . fromValueOf uniA $ fst xy -- [2] + case uniPairAB of + DefaultUniPair uniA _ -> -- [1] + pure . fromValueOf uniA $ fst xy -- [2] + _ -> + throwing _StructuralUnliftingError "Expected a pair but got something else" {-# INLINE fstPairDenotation #-} in makeBuiltinMeaning fstPairDenotation @@ -778,15 +788,17 @@ Note that it's fine to mix automatic unlifting for polymorphism not related to b manual unlifting for arguments having non-monomorphized polymorphic built-in types, for example: toBuiltinMeaning _ ChooseList = - let chooseListDenotation :: SomeConstant uni [a] -> b -> b -> EvaluationResult b + let chooseListDenotation :: SomeConstant uni [a] -> b -> b -> BuiltinResult b chooseListDenotation (SomeConstant (Some (ValueOf uniListA xs))) a b = do - DefaultUniList _ <- pure uniListA - pure $ case xs of - [] -> a - _ : _ -> b + case uniListA of + DefaultUniList _ -> pure $ case xs of + [] -> a + _ : _ -> b + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE chooseListDenotation #-} in makeBuiltinMeaning chooseListDenotation + (runCostingFunThreeArguments . paramChooseList) Here @a@ appears inside @[]@, which is a polymorphic built-in type, and so we have to use @@ -798,13 +810,17 @@ Our final example is this: toBuiltinMeaning _ MkCons = let mkConsDenotation - :: SomeConstant uni a -> SomeConstant uni [a] -> EvaluationResult (Opaque val [a]) + :: SomeConstant uni a -> SomeConstant uni [a] -> BuiltinResult (Opaque val [a]) mkConsDenotation (SomeConstant (Some (ValueOf uniA x))) (SomeConstant (Some (ValueOf uniListA xs))) = do - DefaultUniList uniA' <- pure uniListA -- [1] - Just Refl <- pure $ uniA `geq` uniA' -- [2] - pure . fromValueOf uniListA $ x : xs -- [3] + case uniListA of + DefaultUniList uniA' -> case uniA `geq` uniA' of -- [1] + Just Refl -> -- [2] + pure . fromValueOf uniListA $ x : xs -- [3] + _ -> throwing _StructuralUnliftingError + "The type of the value does not match the type of elements in the list" + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE mkConsDenotation #-} in makeBuiltinMeaning mkConsDenotation @@ -827,8 +843,8 @@ Plutus type of the builtin: get the (Plutus) kind of a builtin head and check two builtin heads for equality 3. Plutus type normalization tears partially or fully instantiated built-in types (such as @[Integer]@) apart and creates a Plutus type application for each Haskell type application -4. 'Emitter' and 'EvaluationResult' do not appear on the Plutus side, since the logging and failure - effects are implicit in Plutus as was discussed above +4. 'BuiltinResult' does not appear on the Plutus side, since the logging and failure effects are + implicit in Plutus as was discussed above 5. 'Opaque' and 'SomeConstant' both carry a Haskell @rep@ type argument representing some Plutus type to be used for Plutus type checking @@ -840,10 +856,10 @@ actually does. Let's look at some examples. toBuiltinMeaning _ IdIntegerAsBool = let idIntegerAsBoolDenotation - :: SomeConstant uni Integer -> EvaluationResult (SomeConstant uni Integer) + :: SomeConstant uni Integer -> BuiltinResult (SomeConstant uni Integer) idIntegerAsBoolDenotation = \case - con@(SomeConstant (Some (ValueOf DefaultUniBool _))) -> EvaluationSuccess con - _ -> EvaluationFailure + con@(SomeConstant (Some (ValueOf DefaultUniBool _))) -> pure con + _ -> evaluationFailure {-# INLINE idIntegerAsBoolDenotation #-} in makeBuiltinMeaning idIntegerAsBoolDenotation @@ -1054,7 +1070,7 @@ Finally, is representable (because we can require arguments to be constants carrying universes with them, which we can use to construct the resulting universe), but is still a lie, because instantiating that builtin with non-built-in types is possible and so the PLC type checker won't throw on such -an instantiation, which will become 'EvalutionFailure' at runtime the moment unlifting of a +an instantiation, which will become 'evalutionFailure' at runtime the moment unlifting of a non-constant is attempted when a constant is expected. So could we still get @nil@ or a safe version of @comma@ somehow? Well, we could have this @@ -1081,6 +1097,12 @@ This was investigated in https://github.com/IntersectMBO/plutus/pull/4337 but we do it quite yet, even though it worked (the Plutus Tx part wasn't implemented). -} +{- Note [Operational vs structural errors within builtins] +See the Haddock of 'EvaluationError' to understand why we sometimes use 'fail' (to throw an +"operational" evaluation error) and sometimes use @throwing _StructuralUnliftingError@ (to throw a +"structural" evaluation error). Please respect the distinction when adding new built-in functions. +-} + instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where type CostingPart uni DefaultFun = BuiltinCostModel @@ -1127,7 +1149,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunTwoArguments . paramMultiplyInteger) toBuiltinMeaning _semvar DivideInteger = - let divideIntegerDenotation :: Integer -> Integer -> EvaluationResult Integer + let divideIntegerDenotation :: Integer -> Integer -> BuiltinResult Integer divideIntegerDenotation = nonZeroSecondArg div {-# INLINE divideIntegerDenotation #-} in makeBuiltinMeaning @@ -1135,7 +1157,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunTwoArguments . paramDivideInteger) toBuiltinMeaning _semvar QuotientInteger = - let quotientIntegerDenotation :: Integer -> Integer -> EvaluationResult Integer + let quotientIntegerDenotation :: Integer -> Integer -> BuiltinResult Integer quotientIntegerDenotation = nonZeroSecondArg quot {-# INLINE quotientIntegerDenotation #-} in makeBuiltinMeaning @@ -1143,7 +1165,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunTwoArguments . paramQuotientInteger) toBuiltinMeaning _semvar RemainderInteger = - let remainderIntegerDenotation :: Integer -> Integer -> EvaluationResult Integer + let remainderIntegerDenotation :: Integer -> Integer -> BuiltinResult Integer remainderIntegerDenotation = nonZeroSecondArg rem {-# INLINE remainderIntegerDenotation #-} in makeBuiltinMeaning @@ -1151,7 +1173,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunTwoArguments . paramRemainderInteger) toBuiltinMeaning _semvar ModInteger = - let modIntegerDenotation :: Integer -> Integer -> EvaluationResult Integer + let modIntegerDenotation :: Integer -> Integer -> BuiltinResult Integer modIntegerDenotation = nonZeroSecondArg mod {-# INLINE modIntegerDenotation #-} in makeBuiltinMeaning @@ -1239,11 +1261,14 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunOneArgument . paramLengthOfByteString) toBuiltinMeaning _semvar IndexByteString = - let indexByteStringDenotation :: BS.ByteString -> Int -> EvaluationResult Word8 + let indexByteStringDenotation :: BS.ByteString -> Int -> BuiltinResult Word8 indexByteStringDenotation xs n = do - -- TODO: fix this mess with @indexMaybe@ from @bytestring >= 0.11.0.0@. - guard $ n >= 0 && n < BS.length xs - EvaluationSuccess $ BS.index xs n + unless (n >= 0 && n < BS.length xs) $ + -- See Note [Operational vs structural errors within builtins]. + -- The arguments are going to be printed in the "cause" part of the error + -- message, so we don't need to repeat them here. + fail "Index out of bounds" + pure $ BS.index xs n {-# INLINE indexByteStringDenotation #-} in makeBuiltinMeaning indexByteStringDenotation @@ -1374,8 +1399,8 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunOneArgument . paramEncodeUtf8) toBuiltinMeaning _semvar DecodeUtf8 = - let decodeUtf8Denotation :: BS.ByteString -> EvaluationResult Text - decodeUtf8Denotation = reoption . decodeUtf8' + let decodeUtf8Denotation :: BS.ByteString -> BuiltinResult Text + decodeUtf8Denotation = eitherToBuiltinResult . decodeUtf8' {-# INLINE decodeUtf8Denotation #-} in makeBuiltinMeaning decodeUtf8Denotation @@ -1401,7 +1426,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where -- Tracing toBuiltinMeaning _semvar Trace = - let traceDenotation :: Text -> a -> Emitter a + let traceDenotation :: Text -> a -> BuiltinResult a traceDenotation text a = a <$ emit text {-# INLINE traceDenotation #-} in makeBuiltinMeaning @@ -1410,20 +1435,26 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where -- Pairs toBuiltinMeaning _semvar FstPair = - let fstPairDenotation :: SomeConstant uni (a, b) -> EvaluationResult (Opaque val a) + let fstPairDenotation :: SomeConstant uni (a, b) -> BuiltinResult (Opaque val a) fstPairDenotation (SomeConstant (Some (ValueOf uniPairAB xy))) = do - DefaultUniPair uniA _ <- pure uniPairAB - pure . fromValueOf uniA $ fst xy + case uniPairAB of + DefaultUniPair uniA _ -> pure . fromValueOf uniA $ fst xy + _ -> + -- See Note [Operational vs structural errors within builtins]. + throwing _StructuralUnliftingError "Expected a pair but got something else" {-# INLINE fstPairDenotation #-} in makeBuiltinMeaning fstPairDenotation (runCostingFunOneArgument . paramFstPair) toBuiltinMeaning _semvar SndPair = - let sndPairDenotation :: SomeConstant uni (a, b) -> EvaluationResult (Opaque val b) + let sndPairDenotation :: SomeConstant uni (a, b) -> BuiltinResult (Opaque val b) sndPairDenotation (SomeConstant (Some (ValueOf uniPairAB xy))) = do - DefaultUniPair _ uniB <- pure uniPairAB - pure . fromValueOf uniB $ snd xy + case uniPairAB of + DefaultUniPair _ uniB -> pure . fromValueOf uniB $ snd xy + _ -> + -- See Note [Operational vs structural errors within builtins]. + throwing _StructuralUnliftingError "Expected a pair but got something else" {-# INLINE sndPairDenotation #-} in makeBuiltinMeaning sndPairDenotation @@ -1431,64 +1462,74 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where -- Lists toBuiltinMeaning _semvar ChooseList = - let chooseListDenotation :: SomeConstant uni [a] -> b -> b -> EvaluationResult b + let chooseListDenotation :: SomeConstant uni [a] -> b -> b -> BuiltinResult b chooseListDenotation (SomeConstant (Some (ValueOf uniListA xs))) a b = do - DefaultUniList _ <- pure uniListA - pure $ case xs of - [] -> a - _ : _ -> b + case uniListA of + DefaultUniList _ -> pure $ case xs of + [] -> a + _ : _ -> b + -- See Note [Operational vs structural errors within builtins]. + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE chooseListDenotation #-} in makeBuiltinMeaning chooseListDenotation (runCostingFunThreeArguments . paramChooseList) toBuiltinMeaning _semvar MkCons = + let mkConsDenotation - :: SomeConstant uni a -> SomeConstant uni [a] -> EvaluationResult (Opaque val [a]) + :: SomeConstant uni a -> SomeConstant uni [a] -> BuiltinResult (Opaque val [a]) mkConsDenotation (SomeConstant (Some (ValueOf uniA x))) (SomeConstant (Some (ValueOf uniListA xs))) = do - DefaultUniList uniA' <- pure uniListA - -- Checking that the type of the constant is the same as the type of the elements - -- of the unlifted list. Note that there's no way we could enforce this statically - -- since in UPLC one can create an ill-typed program that attempts to prepend - -- a value of the wrong type to a list. - -- Should that rather give us an 'UnliftingError'? For that we need - -- https://github.com/IntersectMBO/plutus/pull/3035 - Just Refl <- pure $ uniA `geq` uniA' - pure . fromValueOf uniListA $ x : xs + -- See Note [Operational vs structural errors within builtins]. + case uniListA of + DefaultUniList uniA' -> case uniA `geq` uniA' of + Just Refl -> pure . fromValueOf uniListA $ x : xs + _ -> throwing _StructuralUnliftingError + "The type of the value does not match the type of elements in the list" + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE mkConsDenotation #-} in makeBuiltinMeaning mkConsDenotation (runCostingFunTwoArguments . paramMkCons) toBuiltinMeaning _semvar HeadList = - let headListDenotation :: SomeConstant uni [a] -> EvaluationResult (Opaque val a) + let headListDenotation :: SomeConstant uni [a] -> BuiltinResult (Opaque val a) headListDenotation (SomeConstant (Some (ValueOf uniListA xs))) = do - DefaultUniList uniA <- pure uniListA - x : _ <- pure xs - pure $ fromValueOf uniA x + -- See Note [Operational vs structural errors within builtins]. + case uniListA of + DefaultUniList uniA -> case xs of + [] -> fail "Expected a non-empty list but got an empty one" + x : _ -> pure $ fromValueOf uniA x + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE headListDenotation #-} in makeBuiltinMeaning headListDenotation (runCostingFunOneArgument . paramHeadList) toBuiltinMeaning _semvar TailList = - let tailListDenotation :: SomeConstant uni [a] -> EvaluationResult (Opaque val [a]) + let tailListDenotation :: SomeConstant uni [a] -> BuiltinResult (Opaque val [a]) tailListDenotation (SomeConstant (Some (ValueOf uniListA xs))) = do - DefaultUniList _ <- pure uniListA - _ : xs' <- pure xs - pure $ fromValueOf uniListA xs' + -- See Note [Operational vs structural errors within builtins]. + case uniListA of + DefaultUniList _ -> case xs of + [] -> fail "Expected a non-empty list but got an empty one" + _ : xs' -> pure $ fromValueOf uniListA xs' + _ -> throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE tailListDenotation #-} in makeBuiltinMeaning tailListDenotation (runCostingFunOneArgument . paramTailList) toBuiltinMeaning _semvar NullList = - let nullListDenotation :: SomeConstant uni [a] -> EvaluationResult Bool + let nullListDenotation :: SomeConstant uni [a] -> BuiltinResult Bool nullListDenotation (SomeConstant (Some (ValueOf uniListA xs))) = do - DefaultUniList _ <- pure uniListA - pure $ null xs + case uniListA of + DefaultUniList _ -> pure $ null xs + _ -> + -- See Note [Operational vs structural errors within builtins]. + throwing _StructuralUnliftingError "Expected a list but got something else" {-# INLINE nullListDenotation #-} in makeBuiltinMeaning nullListDenotation @@ -1550,50 +1591,55 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where (runCostingFunOneArgument . paramBData) toBuiltinMeaning _semvar UnConstrData = - let unConstrDataDenotation :: Data -> EvaluationResult (Integer, [Data]) + let unConstrDataDenotation :: Data -> BuiltinResult (Integer, [Data]) unConstrDataDenotation = \case - Constr i ds -> EvaluationSuccess (i, ds) - _ -> EvaluationFailure + Constr i ds -> pure (i, ds) + -- See Note [Operational vs structural errors within builtins]. + _ -> fail "Expected the Constr constructor but got a different one" {-# INLINE unConstrDataDenotation #-} in makeBuiltinMeaning unConstrDataDenotation (runCostingFunOneArgument . paramUnConstrData) toBuiltinMeaning _semvar UnMapData = - let unMapDataDenotation :: Data -> EvaluationResult [(Data, Data)] + let unMapDataDenotation :: Data -> BuiltinResult [(Data, Data)] unMapDataDenotation = \case - Map es -> EvaluationSuccess es - _ -> EvaluationFailure + Map es -> pure es + -- See Note [Operational vs structural errors within builtins]. + _ -> fail "Expected the Map constructor but got a different one" {-# INLINE unMapDataDenotation #-} in makeBuiltinMeaning unMapDataDenotation (runCostingFunOneArgument . paramUnMapData) toBuiltinMeaning _semvar UnListData = - let unListDataDenotation :: Data -> EvaluationResult [Data] + let unListDataDenotation :: Data -> BuiltinResult [Data] unListDataDenotation = \case - List ds -> EvaluationSuccess ds - _ -> EvaluationFailure + List ds -> pure ds + -- See Note [Operational vs structural errors within builtins]. + _ -> fail "Expected the List constructor but got a different one" {-# INLINE unListDataDenotation #-} in makeBuiltinMeaning unListDataDenotation (runCostingFunOneArgument . paramUnListData) toBuiltinMeaning _semvar UnIData = - let unIDataDenotation :: Data -> EvaluationResult Integer + let unIDataDenotation :: Data -> BuiltinResult Integer unIDataDenotation = \case - I i -> EvaluationSuccess i - _ -> EvaluationFailure + I i -> pure i + -- See Note [Operational vs structural errors within builtins]. + _ -> fail "Expected the I constructor but got a different one" {-# INLINE unIDataDenotation #-} in makeBuiltinMeaning unIDataDenotation (runCostingFunOneArgument . paramUnIData) toBuiltinMeaning _semvar UnBData = - let unBDataDenotation :: Data -> EvaluationResult BS.ByteString + let unBDataDenotation :: Data -> BuiltinResult BS.ByteString unBDataDenotation = \case - B b -> EvaluationSuccess b - _ -> EvaluationFailure + B b -> pure b + -- See Note [Operational vs structural errors within builtins]. + _ -> fail "Expected the B constructor but got a different one" {-# INLINE unBDataDenotation #-} in makeBuiltinMeaning unBDataDenotation @@ -1683,8 +1729,8 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where toBuiltinMeaning _semvar Bls12_381_G1_uncompress = let bls12_381_G1_uncompressDenotation - :: BS.ByteString -> Emitter (EvaluationResult BLS12_381.G1.Element) - bls12_381_G1_uncompressDenotation = eitherToEmitter . BLS12_381.G1.uncompress + :: BS.ByteString -> BuiltinResult BLS12_381.G1.Element + bls12_381_G1_uncompressDenotation = eitherToBuiltinResult . BLS12_381.G1.uncompress {-# INLINE bls12_381_G1_uncompressDenotation #-} in makeBuiltinMeaning bls12_381_G1_uncompressDenotation @@ -1692,8 +1738,8 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where toBuiltinMeaning _semvar Bls12_381_G1_hashToGroup = let bls12_381_G1_hashToGroupDenotation - :: BS.ByteString -> BS.ByteString -> Emitter (EvaluationResult BLS12_381.G1.Element) - bls12_381_G1_hashToGroupDenotation = eitherToEmitter .* BLS12_381.G1.hashToGroup + :: BS.ByteString -> BS.ByteString -> BuiltinResult BLS12_381.G1.Element + bls12_381_G1_hashToGroupDenotation = eitherToBuiltinResult .* BLS12_381.G1.hashToGroup {-# INLINE bls12_381_G1_hashToGroupDenotation #-} in makeBuiltinMeaning bls12_381_G1_hashToGroupDenotation @@ -1744,8 +1790,8 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where toBuiltinMeaning _semvar Bls12_381_G2_uncompress = let bls12_381_G2_uncompressDenotation - :: BS.ByteString -> Emitter (EvaluationResult BLS12_381.G2.Element) - bls12_381_G2_uncompressDenotation = eitherToEmitter . BLS12_381.G2.uncompress + :: BS.ByteString -> BuiltinResult BLS12_381.G2.Element + bls12_381_G2_uncompressDenotation = eitherToBuiltinResult . BLS12_381.G2.uncompress {-# INLINE bls12_381_G2_uncompressDenotation #-} in makeBuiltinMeaning bls12_381_G2_uncompressDenotation @@ -1753,8 +1799,8 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where toBuiltinMeaning _semvar Bls12_381_G2_hashToGroup = let bls12_381_G2_hashToGroupDenotation - :: BS.ByteString -> BS.ByteString -> Emitter (EvaluationResult BLS12_381.G2.Element) - bls12_381_G2_hashToGroupDenotation = eitherToEmitter .* BLS12_381.G2.hashToGroup + :: BS.ByteString -> BS.ByteString -> BuiltinResult BLS12_381.G2.Element + bls12_381_G2_hashToGroupDenotation = eitherToBuiltinResult .* BLS12_381.G2.hashToGroup {-# INLINE bls12_381_G2_hashToGroupDenotation #-} in makeBuiltinMeaning bls12_381_G2_hashToGroupDenotation diff --git a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Error.hs b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Error.hs index 0c3e5317e70..84c29ae36da 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Error.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Error.hs @@ -30,11 +30,11 @@ import Data.Bitraversable {- | The type of errors that can occur during evaluation. There are two kinds of errors: 1. Operational ones -- these are errors that are indicative of the _logic_ of the program being - wrong. For example, 'Error' was executed, 'tailList' was applied to an empty list or evaluation + wrong. For example, 'error' was executed, 'tailList' was applied to an empty list or evaluation ran out of gas. 2. Structural ones -- these are errors that are indicative of the _structure_ of the program being - wrong. For example, a free variable was encountered during evaluation, or a non-function was - applied to an argument. + wrong. For example, a free variable was encountered during evaluation, a non-function was + applied to an argument or 'tailList' was applied to a non-list. On the chain both of these are just regular failures and we don't distinguish between them there: if a script fails, it fails, it doesn't matter what the reason was. However in the tests it does @@ -62,19 +62,23 @@ mtraverse makeClassyPrisms instance Bifunctor EvaluationError where bimap f _ (OperationalEvaluationError err) = OperationalEvaluationError $ f err bimap _ g (StructuralEvaluationError err) = StructuralEvaluationError $ g err + {-# INLINE bimap #-} instance Bifoldable EvaluationError where bifoldMap f _ (OperationalEvaluationError err) = f err bifoldMap _ g (StructuralEvaluationError err) = g err + {-# INLINE bifoldMap #-} instance Bitraversable EvaluationError where bitraverse f _ (OperationalEvaluationError err) = OperationalEvaluationError <$> f err bitraverse _ g (StructuralEvaluationError err) = StructuralEvaluationError <$> g err + {-# INLINE bitraverse #-} -- | A raw evaluation failure is always an operational error. instance AsEvaluationFailure operational => AsEvaluationFailure (EvaluationError operational structural) where _EvaluationFailure = _OperationalEvaluationError . _EvaluationFailure + {-# INLINE _EvaluationFailure #-} instance ( HasPrettyDefaults config ~ 'True diff --git a/plutus-core/plutus-core/src/PlutusCore/Evaluation/ErrorWithCause.hs b/plutus-core/plutus-core/src/PlutusCore/Evaluation/ErrorWithCause.hs index f63f2588506..146d74d8b75 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Evaluation/ErrorWithCause.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Evaluation/ErrorWithCause.hs @@ -32,9 +32,11 @@ data ErrorWithCause err cause = ErrorWithCause instance Bifunctor ErrorWithCause where bimap f g (ErrorWithCause err cause) = ErrorWithCause (f err) (g <$> cause) + {-# INLINE bimap #-} instance AsEvaluationFailure err => AsEvaluationFailure (ErrorWithCause err cause) where _EvaluationFailure = iso _ewcError (flip ErrorWithCause Nothing) . _EvaluationFailure + {-# INLINE _EvaluationFailure #-} instance (Pretty err, Pretty cause) => Pretty (ErrorWithCause err cause) where pretty (ErrorWithCause e c) = pretty e <+> "caused by:" <+> pretty c @@ -63,6 +65,7 @@ throwingWithCause :: forall exc e t term m x. (exc ~ ErrorWithCause e term, MonadError exc m) => AReview e t -> t -> Maybe term -> m x throwingWithCause l t cause = reviews l (\e -> throwError $ ErrorWithCause e cause) t +{-# INLINE throwingWithCause #-} -- | "Prismatically" throw a contentless error and its (optional) cause. 'throwingWithCause_' is to -- 'throwingWithCause' as 'throwing_' is to 'throwing'. @@ -71,6 +74,7 @@ throwingWithCause_ :: forall exc e term m x. (exc ~ ErrorWithCause e term, MonadError exc m) => AReview e () -> Maybe term -> m x throwingWithCause_ l = throwingWithCause l () +{-# INLINE throwingWithCause_ #-} -- | Attach a @cause@ to a 'BuiltinError' and throw that. -- Note that an evaluator might require the cause to be computed lazily for best performance on the @@ -86,3 +90,4 @@ throwBuiltinErrorWithCause cause = \case throwingWithCause _UnliftingEvaluationError unlErr $ Just cause BuiltinEvaluationFailure -> throwingWithCause_ _EvaluationFailure $ Just cause +{-# INLINE throwBuiltinErrorWithCause #-} diff --git a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/Exception.hs b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/Exception.hs index c56a4e6faed..8dfa83c1abf 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/Exception.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/Exception.hs @@ -72,9 +72,11 @@ mtraverse makeClassyPrisms instance structural ~ MachineError fun => AsMachineError (EvaluationError operational structural) fun where _MachineError = _StructuralEvaluationError + {-# INLINE _MachineError #-} instance AsUnliftingError (MachineError fun) where _UnliftingError = _UnliftingMachineError + {-# INLINE _UnliftingError #-} type EvaluationException operational structural = ErrorWithCause (EvaluationError operational structural) diff --git a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Result.hs b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Result.hs index 43321ea4c88..caf61ddca47 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Result.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Result.hs @@ -74,6 +74,7 @@ instance AsEvaluationFailure (EvaluationResult a) where _EvaluationFailure = prism (const EvaluationFailure) $ \case a@EvaluationSuccess{} -> Left a EvaluationFailure -> Right () + {-# INLINE _EvaluationFailure #-} -- This and the next one are two instances that allow us to write the following: -- diff --git a/plutus-core/testlib/PlutusCore/Generators/Hedgehog/Interesting.hs b/plutus-core/testlib/PlutusCore/Generators/Hedgehog/Interesting.hs index 509f5faaa25..427279aa8da 100644 --- a/plutus-core/testlib/PlutusCore/Generators/Hedgehog/Interesting.hs +++ b/plutus-core/testlib/PlutusCore/Generators/Hedgehog/Interesting.hs @@ -233,15 +233,15 @@ genApplyAdd2 = do return . TermOf term $ iv + jv -- | Check that division by zero results in 'Error'. -genDivideByZero :: TermGen (EvaluationResult Integer) +genDivideByZero :: TermGen (BuiltinResult Integer) genDivideByZero = do op <- Gen.element [DivideInteger, QuotientInteger, ModInteger, RemainderInteger] TermOf i _ <- genTermLoose $ typeRep @Integer let term = mkIterAppNoAnn (Builtin () op) [i, mkConstant @Integer () 0] - return $ TermOf term EvaluationFailure + return $ TermOf term evaluationFailure -- | Check that division by zero results in 'Error' even if a function doesn't use that argument. -genDivideByZeroDrop :: TermGen (EvaluationResult Integer) +genDivideByZeroDrop :: TermGen (BuiltinResult Integer) genDivideByZeroDrop = do op <- Gen.element [DivideInteger, QuotientInteger, ModInteger, RemainderInteger] let typedInt = typeRep @@ -252,7 +252,7 @@ genDivideByZeroDrop = do [ mkConstant @Integer () iv , mkIterAppNoAnn (Builtin () op) [i, mkConstant @Integer () 0] ] - return $ TermOf term EvaluationFailure + return $ TermOf term evaluationFailure -- | Apply a function to all interesting generators and collect the results. fromInterestingTermGens diff --git a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/headList-empty.err.golden b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/headList-empty.err.golden index 71c540c808a..0f8c3121e2d 100644 --- a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/headList-empty.err.golden +++ b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/headList-empty.err.golden @@ -1,3 +1,5 @@ An error has occurred: The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'. -Caused by: (force headList []) \ No newline at end of file +Caused by: (force headList []) +Logs were: +Expected a non-empty list but got an empty one \ No newline at end of file diff --git a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/tailList-empty.err.golden b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/tailList-empty.err.golden index 679ca697721..7d9ddbf5dc9 100644 --- a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/tailList-empty.err.golden +++ b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/List/tailList-empty.err.golden @@ -1,3 +1,5 @@ An error has occurred: The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'. -Caused by: (force tailList []) \ No newline at end of file +Caused by: (force tailList []) +Logs were: +Expected a non-empty list but got an empty one \ No newline at end of file diff --git a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-empty.err.golden b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-empty.err.golden index 89c63ce9144..7bc851206a0 100644 --- a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-empty.err.golden +++ b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-empty.err.golden @@ -1,3 +1,5 @@ An error has occurred: The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'. -Caused by: (indexByteString # 0) \ No newline at end of file +Caused by: (indexByteString # 0) +Logs were: +Index out of bounds \ No newline at end of file diff --git a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-non-empty.err.golden b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-non-empty.err.golden index fddc0becff1..0347aa759b2 100644 --- a/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-non-empty.err.golden +++ b/plutus-core/untyped-plutus-core/test/Evaluation/Builtins/Golden/String/indexByteString-out-of-bounds-non-empty.err.golden @@ -1,3 +1,5 @@ An error has occurred: The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'. -Caused by: (indexByteString #68656c6c6f20776f726c64 12) \ No newline at end of file +Caused by: (indexByteString #68656c6c6f20776f726c64 12) +Logs were: +Index out of bounds \ No newline at end of file diff --git a/plutus-tx-plugin/test/Plugin/Basic/9.6/ifOptEval.eval.golden b/plutus-tx-plugin/test/Plugin/Basic/9.6/ifOptEval.eval.golden index 8f62cdc8e64..31522decfd6 100644 --- a/plutus-tx-plugin/test/Plugin/Basic/9.6/ifOptEval.eval.golden +++ b/plutus-tx-plugin/test/Plugin/Basic/9.6/ifOptEval.eval.golden @@ -3,4 +3,4 @@ The machine terminated because of an error, either from a built-in function or f Caused by: (divideInteger 1 0) Final budget: ({cpu: 132030 | mem: 101}) -Logs: \ No newline at end of file +Logs: Cannot divide by zero \ No newline at end of file