Skip to content

[Refactoring] Remove prismatic error handling from Ck/Cek machines #7116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025

Conversation

SeungheonOh
Copy link
Collaborator

@SeungheonOh SeungheonOh commented May 27, 2025

Closes #6160

Removes prismatic error handling from Ck/Cek machine implementations.

There is a very minor performance degradation, and I don't know where it's coming from. I'll see if I can dig into ghc core tomorrow

@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 578a6b5 to d8e4539 Compare May 27, 2025 12:31
@SeungheonOh SeungheonOh changed the title Remove prismatic error handling from Ck/Cek machines [Refactoring] Remove prismatic error handling from Ck/Cek machines May 27, 2025
@SeungheonOh SeungheonOh self-assigned this May 27, 2025
@SeungheonOh SeungheonOh added Refactoring No Changelog Required Add this to skip the Changelog Check labels May 27, 2025
@SeungheonOh
Copy link
Collaborator Author

/benchmark validation

Copy link
Contributor

Click here to check the status of your benchmark.

Copy link
Contributor

Comparing benchmark results of 'validation' on 'f17c02cb9' (base) and 'd8e453994' (PR)

Results table
Script f17c02c d8e4539 Change
auction_1-1 162.9 μs 167.5 μs +2.8%
auction_1-2 524.6 μs 542.4 μs +3.4%
auction_1-3 526.4 μs 542.4 μs +3.0%
auction_1-4 210.6 μs 217.8 μs +3.4%
auction_2-1 162.4 μs 167.6 μs +3.2%
auction_2-2 523.2 μs 542.1 μs +3.6%
auction_2-3 678.4 μs 702.6 μs +3.6%
auction_2-4 523.2 μs 538.2 μs +2.9%
auction_2-5 210.7 μs 216.4 μs +2.7%
crowdfunding-success-1 191.3 μs 199.3 μs +4.2%
crowdfunding-success-2 190.6 μs 198.7 μs +4.2%
crowdfunding-success-3 191.1 μs 198.2 μs +3.7%
currency-1 207.7 μs 214.9 μs +3.5%
escrow-redeem_1-1 295.0 μs 310.4 μs +5.2%
escrow-redeem_1-2 294.0 μs 309.0 μs +5.1%
escrow-redeem_2-1 341.8 μs 357.3 μs +4.5%
escrow-redeem_2-2 341.3 μs 357.5 μs +4.7%
escrow-redeem_2-3 341.9 μs 358.4 μs +4.8%
escrow-refund-1 141.2 μs 145.7 μs +3.2%
future-increase-margin-1 208.1 μs 214.5 μs +3.1%
future-increase-margin-2 449.5 μs 470.4 μs +4.6%
future-increase-margin-3 446.5 μs 468.5 μs +4.9%
future-increase-margin-4 405.7 μs 423.4 μs +4.4%
future-increase-margin-5 677.8 μs 704.9 μs +4.0%
future-pay-out-1 208.2 μs 215.0 μs +3.3%
future-pay-out-2 446.2 μs 470.9 μs +5.5%
future-pay-out-3 447.4 μs 475.6 μs +6.3%
future-pay-out-4 674.5 μs 700.0 μs +3.8%
future-settle-early-1 208.2 μs 215.2 μs +3.4%
future-settle-early-2 447.1 μs 470.4 μs +5.2%
future-settle-early-3 446.7 μs 468.4 μs +4.9%
future-settle-early-4 515.1 μs 534.9 μs +3.8%
game-sm-success_1-1 327.1 μs 336.6 μs +2.9%
game-sm-success_1-2 182.5 μs 187.9 μs +3.0%
game-sm-success_1-3 526.0 μs 547.9 μs +4.2%
game-sm-success_1-4 212.0 μs 217.8 μs +2.7%
game-sm-success_2-1 326.9 μs 337.4 μs +3.2%
game-sm-success_2-2 182.8 μs 188.7 μs +3.2%
game-sm-success_2-3 526.3 μs 548.3 μs +4.2%
game-sm-success_2-4 211.5 μs 217.6 μs +2.9%
game-sm-success_2-5 524.9 μs 548.1 μs +4.4%
game-sm-success_2-6 211.6 μs 217.9 μs +3.0%
multisig-sm-1 330.5 μs 342.8 μs +3.7%
multisig-sm-2 322.9 μs 340.2 μs +5.4%
multisig-sm-3 325.4 μs 342.4 μs +5.2%
multisig-sm-4 331.7 μs 341.4 μs +2.9%
multisig-sm-5 456.6 μs 478.5 μs +4.8%
multisig-sm-6 329.9 μs 343.3 μs +4.1%
multisig-sm-7 328.1 μs 339.5 μs +3.5%
multisig-sm-8 327.3 μs 344.0 μs +5.1%
multisig-sm-9 330.3 μs 343.6 μs +4.0%
multisig-sm-10 457.3 μs 478.8 μs +4.7%
ping-pong-1 273.5 μs 282.9 μs +3.4%
ping-pong-2 272.9 μs 283.3 μs +3.8%
ping-pong_2-1 177.9 μs 178.4 μs +0.3%
prism-1 153.6 μs 157.7 μs +2.7%
prism-2 351.8 μs 358.7 μs +2.0%
prism-3 312.5 μs 327.9 μs +4.9%
pubkey-1 130.1 μs 133.7 μs +2.8%
stablecoin_1-1 792.7 μs 814.7 μs +2.8%
stablecoin_1-2 177.7 μs 183.8 μs +3.4%
stablecoin_1-3 907.8 μs 930.3 μs +2.5%
stablecoin_1-4 188.3 μs 195.1 μs +3.6%
stablecoin_1-5 1.150 ms 1.185 ms +3.0%
stablecoin_1-6 231.6 μs 239.2 μs +3.3%
stablecoin_2-1 792.7 μs 813.8 μs +2.7%
stablecoin_2-2 178.2 μs 186.6 μs +4.7%
stablecoin_2-3 910.1 μs 930.6 μs +2.3%
stablecoin_2-4 188.5 μs 195.3 μs +3.6%
token-account-1 162.8 μs 166.6 μs +2.3%
token-account-2 289.3 μs 296.4 μs +2.5%
uniswap-1 334.1 μs 344.7 μs +3.2%
uniswap-2 192.4 μs 197.8 μs +2.8%
uniswap-3 1.434 ms 1.484 ms +3.5%
uniswap-4 300.9 μs 309.9 μs +3.0%
uniswap-5 966.0 μs 998.3 μs +3.3%
uniswap-6 283.5 μs 293.4 μs +3.5%
vesting-1 290.8 μs 299.5 μs +3.0%
f17c02c d8e4539 Change
TOTAL 29.85 ms 30.95 ms +3.7%

@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from d8e4539 to d22e54a Compare May 27, 2025 15:45
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from d22e54a to ab0ef1b Compare May 27, 2025 16:09
@IntersectMBO IntersectMBO deleted a comment from github-actions bot May 27, 2025
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from ab0ef1b to 65b1759 Compare May 27, 2025 16:50
@SeungheonOh
Copy link
Collaborator Author

/benchmark nofib

@IntersectMBO IntersectMBO deleted a comment from github-actions bot May 27, 2025
Copy link
Contributor

Click here to check the status of your benchmark.

@SeungheonOh SeungheonOh requested a review from effectfully May 27, 2025 17:02
Copy link
Contributor

Comparing benchmark results of 'validation' on 'f17c02cb9' (base) and '65b17597f' (PR)

Results table
Script f17c02c 65b1759 Change
auction_1-1 163.2 μs 163.0 μs -0.1%
auction_1-2 525.1 μs 531.3 μs +1.2%
auction_1-3 525.5 μs 527.3 μs +0.3%
auction_1-4 210.4 μs 210.2 μs -0.1%
auction_2-1 162.3 μs 162.6 μs +0.2%
auction_2-2 523.7 μs 528.6 μs +0.9%
auction_2-3 679.5 μs 688.3 μs +1.3%
auction_2-4 525.5 μs 525.5 μs 0.0%
auction_2-5 210.5 μs 209.8 μs -0.3%
crowdfunding-success-1 191.9 μs 193.4 μs +0.8%
crowdfunding-success-2 191.3 μs 193.0 μs +0.9%
crowdfunding-success-3 191.1 μs 193.4 μs +1.2%
currency-1 208.4 μs 209.1 μs +0.3%
escrow-redeem_1-1 295.2 μs 302.3 μs +2.4%
escrow-redeem_1-2 295.1 μs 301.3 μs +2.1%
escrow-redeem_2-1 343.4 μs 349.0 μs +1.6%
escrow-redeem_2-2 343.8 μs 349.4 μs +1.6%
escrow-redeem_2-3 341.8 μs 348.4 μs +1.9%
escrow-refund-1 141.1 μs 143.2 μs +1.5%
future-increase-margin-1 207.6 μs 208.9 μs +0.6%
future-increase-margin-2 447.2 μs 458.7 μs +2.6%
future-increase-margin-3 446.2 μs 458.5 μs +2.8%
future-increase-margin-4 406.2 μs 410.6 μs +1.1%
future-increase-margin-5 679.4 μs 689.3 μs +1.5%
future-pay-out-1 208.3 μs 209.5 μs +0.6%
future-pay-out-2 448.8 μs 459.1 μs +2.3%
future-pay-out-3 447.0 μs 456.9 μs +2.2%
future-pay-out-4 673.5 μs 684.5 μs +1.6%
future-settle-early-1 207.6 μs 209.2 μs +0.8%
future-settle-early-2 449.1 μs 459.0 μs +2.2%
future-settle-early-3 448.2 μs 459.7 μs +2.6%
future-settle-early-4 519.1 μs 521.9 μs +0.5%
game-sm-success_1-1 326.9 μs 328.9 μs +0.6%
game-sm-success_1-2 182.5 μs 183.0 μs +0.3%
game-sm-success_1-3 525.3 μs 531.8 μs +1.2%
game-sm-success_1-4 211.8 μs 212.4 μs +0.3%
game-sm-success_2-1 328.7 μs 329.7 μs +0.3%
game-sm-success_2-2 182.4 μs 184.7 μs +1.3%
game-sm-success_2-3 524.9 μs 533.9 μs +1.7%
game-sm-success_2-4 211.2 μs 212.6 μs +0.7%
game-sm-success_2-5 525.6 μs 533.6 μs +1.5%
game-sm-success_2-6 211.5 μs 213.4 μs +0.9%
multisig-sm-1 332.0 μs 333.8 μs +0.5%
multisig-sm-2 323.3 μs 331.6 μs +2.6%
multisig-sm-3 326.0 μs 333.4 μs +2.3%
multisig-sm-4 330.5 μs 330.8 μs +0.1%
multisig-sm-5 458.9 μs 464.3 μs +1.2%
multisig-sm-6 330.2 μs 333.2 μs +0.9%
multisig-sm-7 323.4 μs 329.5 μs +1.9%
multisig-sm-8 327.9 μs 333.4 μs +1.7%
multisig-sm-9 337.1 μs 332.1 μs -1.5%
multisig-sm-10 458.3 μs 464.3 μs +1.3%
ping-pong-1 274.3 μs 277.1 μs +1.0%
ping-pong-2 273.8 μs 278.6 μs +1.8%
ping-pong_2-1 175.6 μs 174.5 μs -0.6%
prism-1 154.0 μs 152.1 μs -1.2%
prism-2 351.3 μs 351.2 μs -0.0%
prism-3 313.6 μs 318.9 μs +1.7%
pubkey-1 130.3 μs 131.4 μs +0.8%
stablecoin_1-1 792.8 μs 800.3 μs +0.9%
stablecoin_1-2 178.3 μs 179.1 μs +0.4%
stablecoin_1-3 911.5 μs 921.3 μs +1.1%
stablecoin_1-4 188.5 μs 190.4 μs +1.0%
stablecoin_1-5 1.153 ms 1.170 ms +1.5%
stablecoin_1-6 231.4 μs 234.2 μs +1.2%
stablecoin_2-1 792.8 μs 801.2 μs +1.1%
stablecoin_2-2 178.0 μs 178.7 μs +0.4%
stablecoin_2-3 910.8 μs 916.5 μs +0.6%
stablecoin_2-4 188.4 μs 190.4 μs +1.1%
token-account-1 162.5 μs 162.0 μs -0.3%
token-account-2 288.8 μs 288.8 μs 0.0%
uniswap-1 333.6 μs 334.2 μs +0.2%
uniswap-2 192.3 μs 193.1 μs +0.4%
uniswap-3 1.438 ms 1.457 ms +1.3%
uniswap-4 299.6 μs 300.5 μs +0.3%
uniswap-5 965.6 μs 983.0 μs +1.8%
uniswap-6 283.0 μs 282.5 μs -0.2%
vesting-1 290.9 μs 296.2 μs +1.8%
f17c02c 65b1759 Change
TOTAL 29.89 ms 30.23 ms +1.1%

Copy link
Contributor

Click here to check the status of your benchmark.

@SeungheonOh
Copy link
Collaborator Author

I think this is in reasonable margins of error.

Copy link
Contributor

Comparing benchmark results of 'nofib' on 'f17c02cb9' (base) and '65b17597f' (PR)

Results table
Script f17c02c 65b1759 Change
clausify/formula1 2.223 ms 2.250 ms +1.2%
clausify/formula2 2.984 ms 3.038 ms +1.8%
clausify/formula3 8.264 ms 8.377 ms +1.4%
clausify/formula4 17.72 ms 17.85 ms +0.7%
clausify/formula5 40.15 ms 40.76 ms +1.5%
knights/4x4 14.74 ms 15.21 ms +3.2%
knights/6x6 35.95 ms 36.88 ms +2.6%
knights/8x8 62.49 ms 64.08 ms +2.5%
primetest/05digits 5.407 ms 5.452 ms +0.8%
primetest/10digits 10.63 ms 10.68 ms +0.5%
primetest/30digits 30.91 ms 31.13 ms +0.7%
primetest/50digits 50.62 ms 50.29 ms -0.7%
queens4x4/bt 4.084 ms 4.159 ms +1.8%
queens4x4/bm 5.084 ms 5.179 ms +1.9%
queens4x4/bjbt1 4.874 ms 4.960 ms +1.8%
queens4x4/bjbt2 4.609 ms 4.677 ms +1.5%
queens4x4/fc 10.16 ms 10.29 ms +1.3%
queens5x5/bt 56.94 ms 57.57 ms +1.1%
queens5x5/bm 58.36 ms 58.81 ms +0.8%
queens5x5/bjbt1 66.18 ms 66.83 ms +1.0%
queens5x5/bjbt2 64.37 ms 65.01 ms +1.0%
queens5x5/fc 129.2 ms 130.6 ms +1.1%
f17c02c 65b1759 Change
TOTAL 685.9 ms 694.1 ms +1.2%

@@ -199,7 +198,7 @@ instance tyname ~ TyName => KnownTypeAst tyname DefaultUni Void where
instance UniOf term ~ DefaultUni => MakeKnownIn DefaultUni term Void where
makeKnown = absurd
instance UniOf term ~ DefaultUni => ReadKnownIn DefaultUni term Void where
readKnown _ = throwing _StructuralUnliftingError "Can't unlift to 'Void'"
readKnown _ = throwStructuralUnliftingError "Can't unlift to 'Void'"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd decouple error construction from throwing:

Suggested change
readKnown _ = throwStructuralUnliftingError "Can't unlift to 'Void'"
readKnown _ = throwError $ structuralUnliftingError "Can't unlift to 'Void'"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea

Comment on lines 364 to 370
-- | Same as 'readKnown', but the cause of a potential failure is the provided term itself.
readKnownSelf
:: (ReadKnown val a, AsUnliftingEvaluationError err, AsEvaluationFailure err)
=> val -> Either (ErrorWithCause err val) a
readKnownSelf val = fromRightM (throwBuiltinErrorWithCause val) $ readKnown val
{-# INLINE readKnownSelf #-}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change related to the task or is it an opportunistic improvement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to define this function without prismatic error type constraint unless I have a copy of this function per error type.

Instead, I removed this function entirely and handled error when readKnown is used where error type is actually realized

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha! Thanks for the clarification.

@@ -401,7 +401,7 @@ instance (KnownBuiltinTypeIn DefaultUni term Integer, Integral a, Bounded a, Typ
-- TODO: benchmark an alternative 'integerToIntMaybe', modified from @ghc-bignum@
if fromIntegral (minBound :: a) <= i && i <= fromIntegral (maxBound :: a)
then pure . AsInteger $ fromIntegral i
else throwing _OperationalUnliftingError . MkUnliftingError $ fold
else throwOperationalUnliftingError . MkUnliftingError $ fold
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
else throwOperationalUnliftingError . MkUnliftingError $ fold
else throwError . mkUnliftingError $ fold

(Don't apply, this is an example of idea)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be throwError . operationalUnliftingError . mkUnliftingError but I get the idea

Copy link
Contributor

@Unisay Unisay May 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What sticks into my eye is the repetion of UnliftingError in operationalUnliftingError . mkUnliftingError.

Given 2 facts:

  1. operationalUnliftingError is already UnliftingError-aware.
  2. There is only one constructor to create an unlifting error: MkUnliftingError.

I infer that operationalUnliftingError couldn't be composed with anything other than the one and only MkUnliftingError.

It seems to make sense to squash operationalUnliftingError . mkUnliftingError into
mkOperationalUnliftingError as a first step once (freeing users from doing it over and over again), and then realising that UnliftingError is always operational and this knowledge doesn't need to be repeated per-usage, it could be simplified even further down to mkUnliftingError (but with a composed type), wdyt?

Alternatively,
how about more radical naming throwError . operational . unlifting ? Reads very nice in context.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes UnliftingError ~ Text anyways, and on other cases we were just relying on IsString UnliftingError anyways. Making operationalUnliftingError :: Text -> BuiltinError would be most reasonable; I agree.

:: Maybe (Term TyName Name uni fun ())
-> BuiltinError
-> CkEvaluationException uni fun
builtinErrorToCkEvaluationException cause =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation: you're not factoring in throwing an exception here (which arguably makes more sense for exception than for errors!)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally prevented myself from making any semantic changes on error types.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't mean to change the CkEvaluationException type (I could have, but that wasn't my point).
I meant this direction of change builtinErrorToCkEvaluationException -> throwBuiltinErrorToCkEvaluationException.
The PR doesn't follow this path consistently.

Copy link
Contributor

@Unisay Unisay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for demonstrating that prisms boilerplate could be simplified away without remorse!

My main concern is about decoupling error construction from error throwing:
I much prefer a set of "lifting" constructors that don't throw, e.g. throwError . fooBarBazError instead of throwFooBarBazError as its more compositional without much syntactic difference.

@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 65b1759 to dddf411 Compare May 28, 2025 18:31
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from dddf411 to 57ceadc Compare May 28, 2025 18:32
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 57ceadc to 2652bf4 Compare May 28, 2025 18:36
Copy link
Contributor

@effectfully effectfully left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unhappy about the loss of throwErrorWithCause and readKnownSelf and I think we can fairly easily recover them, see below.

Otherwise looks great to me.

ErrorWithCause
(bimap UnliftingMachineError (const CekEvaluationFailure) $ unUnliftingEvaluationError err)
cause
BuiltinEvaluationFailure -> ErrorWithCause (OperationalError CekEvaluationFailure) cause
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if we refactor it slightly, we'll get

builtinErrorToCekEvaluationException cause = flip ErrorWithCause cause . \case
    BuiltinUnliftingEvaluationError err ->
        bimap UnliftingMachineError (const CekEvaluationFailure) $ unUnliftingEvaluationError err
    BuiltinEvaluationFailure -> OperationalError CekEvaluationFailure

at which point we see that there's a conversion between BuiltinError and EvaluationError structural operational for certain structural and operational.

So could you please turn that into a type class? Just this conversion. And then we can have nice things again (throwBuiltinErrorWithCause and readKnownSelf).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm personally not a huge fan of adding a whole type class for this, but unless we want to change(or merge) CekUserError and CkUserError, I can't think of a better way without code duplication I previously had.

Copy link
Contributor

@effectfully effectfully May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the code duplication that I particularly hate here, it's that it propagates into user space in a non-trivial way (where by "user" I mostly mean "someone who writes tests"). readKnownSelf is a handy thing, I don't want to lose it. If it was only about the machines, I'd swallow the pill.

We can't unify CkEvaluationError and CekEvaluationError into a single type. It's pretty much a coincidence that both are based on MachineError, we could've had (and did have) an evaluator that isn't an abstract machine. Plus, CekUserError and CkUserError do differ, since the CK machine can't with an out-of-budget error.

@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 2652bf4 to 6ba24f0 Compare May 30, 2025 02:21
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 6ba24f0 to b691ee1 Compare May 30, 2025 02:25
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from b691ee1 to 905303e Compare May 30, 2025 02:45
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 905303e to 901578a Compare May 30, 2025 06:56
@SeungheonOh SeungheonOh force-pushed the seungheonoh/6160-evaluation branch from 901578a to 80e2c5d Compare June 2, 2025 13:24
@SeungheonOh
Copy link
Collaborator Author

@effectfully We can merge this if everything looks good

Copy link
Contributor

@effectfully effectfully left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! LGTM.

@kwxm kwxm merged commit 4719134 into master Jun 3, 2025
24 of 27 checks passed
@kwxm kwxm deleted the seungheonoh/6160-evaluation branch June 3, 2025 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
No Changelog Required Add this to skip the Changelog Check Refactoring
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Get rid of prismatic error handling?
4 participants