Skip to content
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

Fix try-catch scoping bug in daml-script #16190

Merged
merged 7 commits into from Feb 6, 2023
Merged

Conversation

akrmn
Copy link
Contributor

@akrmn akrmn commented Jan 30, 2023

Fixes #16132, see #16190 (comment) for explanation

@akrmn akrmn changed the title Akrmn/script abort rework Fix try-catch scoping bug in daml-script Jan 31, 2023
@@ -156,8 +157,9 @@ data ScriptF a
-- (try x catch f) >>= f should not pull `f` in the try block
data CatchPayload a = CatchPayload
with
act : () -> Free ScriptF (a, ())
handle : AnyException -> Optional a
act : () -> Free ScriptF (Free ScriptF LedgerValue, ())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can simplify this to

- act : () -> Free ScriptF (Free ScriptF LedgerValue, ())
+ act : () -> Free ScriptF LedgerValue

but I'd rather do that separately so the fix is easier to understand

@akrmn akrmn marked this pull request as ready for review January 31, 2023 11:38
@akrmn akrmn requested review from cocreature and a team January 31, 2023 11:38
Comment on lines 161 to 166
data CatchPayload a = CatchPayload
with
act : () -> Free ScriptF (a, ())
handle : AnyException -> Optional a
-- TODO(MA): simplify to `act : () -> Free ScriptF LedgerValue`
act : () -> Free ScriptF (Free ScriptF LedgerValue, ())
handle : AnyException -> Optional (Free ScriptF LedgerValue)
continue : LedgerValue -> a
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The type argument a no longer appears in fields act and handle; now it's been replaced by Free ScriptF LedgerValue. This matches the type when used inside Script x, namely a ~ Free ScriptF x, except x is wrapped into a LedgerValue.

This means that instance Functor CatchPayload now only maps over the new field continue, which is only evaluated and run after act (and after handle if act threw an exception). The old behaviour, namely mapping over act and handle was the root of the problem; it meant that all actions after the try-catch block were added to both fields, and if one of the actions was a bare error/fail/abort, the outer Free ScriptF in act would crash while trying to construct the inner Free ScriptF. This last part explains why the bug didn't happen when using throw, since the inner Free ScriptF could be constructed with a ScriptF.Throw to be evaluated in the following runner iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another fix would have been to change the type of act,

- act : () -> Free ScriptF (a, ())
+ act : () -> Free ScriptF (() -> a, ())

recall that inside Script x a ~ Free ScriptF x, giving us

- act : () -> Free ScriptF (Free ScriptF x, ())
+ act : () -> Free ScriptF (() -> Free ScriptF x, ())

with that change, the inner computation in act doesn't need to be constructed until applied to (), so the exception cannot be caught. However, that approach would still duplicate all the actions after the try-catch block into both act and handle, so i opted against it.

Copy link
Contributor

@nickchapman-da nickchapman-da left a comment

Choose a reason for hiding this comment

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

Awesome fix!
LGTM (to the extent I understand it)

My only comment is a question regarding why we now need to manufacture LedgerValue from within the Daml code side. (using toLedgerVaue)

Previously LedgerValues only came from the scala side, and we used fromLedgerValue because in the Daml we know what the a is.

@@ -579,6 +587,9 @@ data LedgerValue = LedgerValue {}
fromLedgerValue : LedgerValue -> a
fromLedgerValue = error "foobar" -- gets replaced by the identity-function in script/Runner.scala

toLedgerValue : a -> LedgerValue
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm slightly puzzled why we need this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I needed this to write the CatchPayload in instance ActionCatch Script. Since it's inside lift $ Free $ Catch $ _, it needs to have type CatchPayload (Free ScriptF t) (where t is the return type of the Script). For context this is what the datatype looks like (in this PR):

data CatchPayload a = CatchPayload
  with
    act : () -> Free ScriptF (Free ScriptF LedgerValue, ())
    handle : AnyException -> Optional (Free ScriptF LedgerValue)
    continue : Free ScriptF LedgerValue -> a

really, we would like to say that the LedgerValue returned by act, the LedgerValue possibly returned by handle, and the LedgerValue consumed by continue are all the same type, let's call it x, for something like

data CatchPayload x a = CatchPayload
  with
    act : () -> Free ScriptF (Free ScriptF x, ())
    handle : AnyException -> Optional (Free ScriptF x)
    continue : Free ScriptF x -> a

the problem with that is that then it's not clear what to put in for x in the Catch constructor of ScriptF:

data ScriptF a 
  = Submit (SubmitCmd a)
  | {- ... -}
  | Catch (CatchPayload (???) a)

what we would want here is to say any type is fine, since what really matters is that all three fields agree on what it is. In Haskell we could use an existential type for exactly that, but those aren't allowed in Daml. However we do have LedgerValue, a fake type representing a value of some type, which is pretty close to what we want. Since we're the ones constructing the CatchPayload, we can make sure that they agree, and then toLedgerValue lets us convince the typechecker.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

prompted by this I've changed the encoding a bit. Now CatchPayload is defined as above with the x argument, and instead of constructing it by using {from,to}LedgerValue in each field, we construct it with type CatchPayload t (Free ScriptF t) and then cast it to CatchPayload LedgerValue (Free ScriptF t) using a new fake function castCatchPayload : CatchPayload x a -> CatchPayload LedgerValue a (using the same id trick). This removes the need for toLedgerValue and gives us a bit more confidence that the types make sense before the cast.

Copy link
Collaborator

@remyhaemmerle-da remyhaemmerle-da left a comment

Choose a reason for hiding this comment

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

This look good for me, however I would like @cocreature to have a look at it. (let him few days before merging)

Copy link
Contributor

@cocreature cocreature left a comment

Choose a reason for hiding this comment

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

Nice! Thanks for tracking this down

@akrmn akrmn merged commit 9582832 into main Feb 6, 2023
@akrmn akrmn deleted the akrmn/script-abort-rework branch February 6, 2023 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Buggy exception handling in Daml Script
4 participants