-
Notifications
You must be signed in to change notification settings - Fork 124
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
Sum types #1588
Comments
I tried to focus on the first case (how to deal with failure) and came up with the following based on the structural variant:
How would one coalesce a sum type? In the example above, this is relevant to implementations of |
What do you mean by "coalesce a sum type"? Judging from what the type signatures for
In order to implement this, I think would would need to introduce a mechanism to pattern-match on a sum type. (I left that out of my original comment in #1588 (comment) to reflect this.) Here is one possible syntax that would work for structural sum types, inspired by OCaml:
|
Thanks for the possible syntax -- Would there be some way to work this into the same notation as numeric constraint guards? After looking at your proposal, the two feel very similar to me. |
Possibly. Currently, the notation of numeric constraint guards only allows writing constraints after each
I haven't thought deeply about what the consequences of generalizing numeric constraint guard notation in this way would be. (My use of the |
Perhaps it would be helpful to have a concrete example. KW-AD is defined on page 14 of NIST SP 800-38F. The algorithm includes a failure condition and is paraphrased here:
For purposes of figuring out how to fail gracefully, here is a partial implementation of KW-AD that elides the definition of
I think this exhibits the behavior we're trying to avoid with this sum types proposal, namely, returning a failure flag along with some partial plaintext ( One way to meet the intent of "return
This style encourages developers to fail cleanly, and also supports both positive and negative testing, and I'm proposing that this style meets the intent of @RyanGlScott's first bullet. I'm not sure Ideally, we'd want something like the following:
This style avoids the use of a failure flag by stopping Cryptol execution on error, encourages clean failure, but does not permit negative testing (as Cryptol and SAW have no way to 'catch' failure). I'd like to hear some ideas on how sum types could be used to make this last example better. Any thoughts would be appreciated. |
This code is a perfect example of (1) from #1588 (comment), where failure is awkwardly represented with a
Alternatively, here is how one might do this with an anonymous sum type:
(Syntax subject to change, of course.) Importantly, the failure case no longer has to use |
Will you provide an example of a function that calls each of your |
I'm feeling like we can get the same thing as nominal
|
If
|
I've never used
An example using a structural (anonymous) sum type:
Certainly, I didn't mean to suggest that it is impossible to define
Sum types are great because they make it straightforward to capture the domain of your problem without introducing unrepresentable states. In a pinch, you can write your code without them, but not without introducing additional complications. |
@RyanGlScott Thanks for the discussion so far! Right now I'm really liking the nominal sum type over the anonymous sum type. Thinking more about Rust and seeing the However, I'm not yet convinced that the addition of sum types will be beneficial. A DSL is great, in part, to the things left out of the language. Aside from the occasional failure condition, I'm having trouble thinking of cryptographic primitives that would benefit from sum types. More examples of crypto needing this feature would be helpful. And...perhaps it's not Cryptol that needs sum types, but SAW. SAW is becoming more and more capable, and is now being used to verify programs that are outside the domain of cryptography. Is Cryptol's narrow scope of 'cryptographic primitives' becoming a liability on SAW's growth? Is it worth thinking about a DSL for systems program verification? |
Most of the discussion above has been about challenge (1) in #1588 (comment). If that were the only challenge, then I'd agree with you that it wouldn't be worth adding sum types to Cryptol. The thing that finally changed my mind was challenge (2), namely, the challenge of how to interface with Rust code in SAW. In Rust, it is idiomatic to write cryptographic primitives that can potentially fail using We are increasingly looking to use SAW to verify such Rust code. Part of this means that we will need to specify what the function returns using Cryptol values (e.g., But now we run into a problem: the Cryptol type This is ultimately what convinced me that life would be much easier if we had sum types in Cryptol. If we did that, then we wouldn't have to bother with these sorts of elaborate sum-type encoding tricks—we could just write the Cryptol specifications in a way that closely resemble the intentions of the original Rust code. This would greatly simplify both the internals of SAW as well as the user experience for users of the SAW Rust backend.
I don't see it that way. I largely agree that having a narrowly scoped DSL makes life better most of the time. And while it's true that Cryptol's scope is largely dictated by the needs of cryptography, that is enough to go a long way in practice. I see the addition of sum types to Cryptol not as a sign that Cryptol's scope was too limited, but as a sign that the cryptographic community is increasingly embracing programming-languages techniques such as sum types. In this sense, I see sum types as a natural addition to Cryptol. Folks writing cryptographic code in Rust have adopted the convention of using sum types, and it's only natural for Cryptol to follow suit. |
Sum-types would be cool indeed. One aspect to consider is verification. SBV's Verification with sum-types is well supported by SMT solvers; especially since you don't plan to support recursive types. (If you allowed for recursion, most solvers would choke for most problems as they don't do induction.) Nominal/Structural: Not much difference from verification perspective; though perhaps nominal would be a bit easier to translate back and from the symbolic world. |
The main use cases I am aware of are:
We can kind of do this in the moment by representing sums with a product, and an index indicating which of the fields in the product is valid. This is quite clunky and a bit of a hack though. Here is a summary of the design I have in mind:
This is similar to Haskell, except that we use
Again, note that we use
I am not 100% sure on what notation to use to distinguish constructors from normal functions, but I think it is a good idea to do so. Using |
Thanks, @yav! I'm broadly in support here, although I do have a couple of follow-up questions:
I'm not necessarily opposed to this idea, but I'm surprised that we'd need to prefix
I'm not sure the rest of your comment ever explained this point—can you elaborate? There's also a discrepancy with newtypes here, since you could partially apply the |
There are a two choices here:
So it might be a bit odd if use of constructors in patterns and expressions is a bit different, but I perhaps it makes sense. Come to think of it, another benefit of just using the module system would be that we could allow |
Actually, it occurs to me that we could adapt the Haskell capitalization convention also, like this:
I think then we don't need any extra punctuation, and things should look fairly natural. |
IMO having An alternative to the |
Another alternative, which I should mention for completeness's sake, is doing away with
Would also generate the following function alongside it (naming conventions up for grabs):
For instance, this
Could be rewritten using
This would dodge the question of needing additional syntax for |
I think the |
#1602 implements non-recursive, nominal sum types (without the |
I propose that we add native support for sum types to the Cryptol language. This would be of particular use in two places:
When specifying algorithms that can possibly fail, one must currently indicate this with a return type that resembles
([32], Bit)
, where theBit
isTrue
if the algorithm succeeded andFalse
otherwise. This is clunky, however, since one must still provide a return value of type[32]
in theFalse
case, even though that return value is useless. What's worse, depending on how the algorithm is defined, returning a partial result of type[32]
might potentially leak sensitive information that was provided to the algorithm.Both of these issues could be fixed by changing the return type to something like
Option u32
, whereOption
is a sum type that has one variant with a single field (in this case, of typeu32
) to represent success, and another variant with no fields at all to represent failure. This way, the failure case does not reveal anything about the inputs at all.In SAW's MIR backend, it would be convenient to embed MIR
enum
values (which are effectively sum types themselves) into Cryptol and back. There isn't a particularly clean way to do this in general, aside from encoding sum types using some sort of Cryptol primitives. Having native sum types as part of Cryptol would make this process much smoother.Having discussed this on @yav, we are both of the opinion that sum types would be useful to add, but we would first need to agree upon a particular design. Here are some specific design questions that would need to be answered:
Should sum types be nominal or structural? That is, should we implement tagged unions (nominal), perhaps resembling this Haskell-inspired syntax?
Or should we implement anonymous variants (structural), similar to how Cryptol's records are anonymous?
@yav believes nominal sum types would be somewhat easier to implement, but also that structural sum types may be a better fit with the design of Cryptol.
If we implement nominal sum types, should we permit recursive types, such as in this example?
@yav is inclined not to allow this.
If we implement structural sum types, should we permit subtyping? That is, should a value of type
( [32] | [64] )
be permitted to be used in a function that expects an argument of type( [32] | [64] | [128] )
? Furthermore, should we permit polymorphic variants such as( [32] | [64] | ... )
, which indicate that other variants are permitted provided that( [32] | [64] )
are included?Again, @yav is inclined not to allow subtyping or polymorphic variants. That is, do the simplest possible thing of requiring the sum types to line up exactly as written.
If we implement structural sum types, how should they interact with type inference? Suppose that a user writes this (I will invent a syntax for introducing values within particular variants):
What should the return type of
f
be? It could reasonably be( [32] | [64] )
,( [32] | [64] | [128] )
, or, if variant polymorphism is permitted, something like( [32] | [64] | ... )
.@yav suggests inferring the smallest set of variants used by a function, which would mean that Cryptol would infer the type
Bit -> ( [32] | [64] )
forf
.What should the pattern-matching mechanism be? (Sum types #1588 (comment) is one possibility.)
The text was updated successfully, but these errors were encountered: