Add a new vector / bitvector library for the Rocq exporter - #3360
Add a new vector / bitvector library for the Rocq exporter#3360sauclovian-g wants to merge 4 commits into
Conversation
|
This is likely to change a good deal still, and it hasn't been integrated at all yet, but the core of it's ready to be looked at. |
|
Before I leave a more detailed review, I have some general questions:
|
|
In the discussion thread for #3202, we talked about how it would be better to map the SAWCore |
|
|
|
On the stdlib vectors... the goal was to create something independent. Note that the warning specifically says that using the stdlib vectors is difficult. There are quite a few reasons for this that extend beyond the form of the We could conceivably share the base type under the covers, but I don't think it's a great idea; it creates a bunch of complications and doesn't really buy much of anything. Also, I've been thinking I should change the vectors to be snoc-based instead of cons-based. This doesn't matter much for the vectors themselves, but for bitvectors it's most natural to have the least significant bit be at the near end of the list; with cons-based vectors this puts bitvectors in backwards order left-to-right, which is confusing under the best of circumstances and worse when using vector tools like On the broader topic of the choice of formulation: there are more issues involved than that comment acknowledges. First, as long as we continue to encode the lengths of vectors in their types, as far as the library user is concerned all the accompanying headaches (e.g. the coercions) will still be present. In principle we could move away from this, and treat the lengths as premises to be carried around in proofs about the code instead. But I don't think that's really a good idea; it would require a near-complete rewrite of the exporter. Plus since Cryptol handles the lengths as type material it's probably better for the exported code to do so too. This means that we can't wish away the problem entirely as some of the comments elsewhere in the stdlib vectors seem to think one should. Second, the best way to deal with large but finite objects is to figure out a way to construct them so that they're finite without that being a huge mess. Then they're just values. You can do this if all you really care about is normal-sized bitvectors (8, 16, 32, 64, etc.) which can all just be separate types, but it doesn't really work if you want the size to be a parameter. Or at least I haven't figured out a way to. Absent that, there are two choices. You can carry around the size as an index type (as the stdlib vectors and this code do), or you can carry around an unbounded object and bundle a proof of the size with it. That's what the comment in the stdlib vectors code suggests, but the comment makes it sound like it has no downsides. That's not true. One cost is that once you have values containing proofs, you get to choose between not using |
|
Thanks for the feedback! I have thoughts on basically every point you raise, so I'll respond to each point below. I do want to emphasize that I'm not trying to be nitpicky here, but rather to make sure that we've carefully thought through the tradeoffs involved in picking a vector representation. We don't get many opportunities to make huge backwards-incompatible changes like this one, so I want to make sure we feel good about the choice we'll make before breaking things! On stdlib vectors:
Can you elaborate more on this? In particular, I want to point out that this does buy us something: namely, that we can reuse a lot of the machinery that the stdlib already defines. For instance, we wouldn't need to redefine
Perhaps I'm misunderstanding you, but isn't it already the case that the least significant bit is near the end of the list? for instance, On the formulation of the vector type:
Can you elaborate more on this? I'm unclear why using lists paired with proofs about their lengths would be more work than the work you've put into this PR. In principle, it should be possible to give the functions in the
I'm not sure what you mean here, mainly because I'm not sure what "type material" means in a Rocq context. Since Rocq is dependently typed, the distinction between types and values isn't as clear as it is in Cryptol.
Again, I'm not sure what you mean here. It is certainly possible for the lists-paired-with-proofs approach to be parametric over the list size. I think I need to see an example of what sort of difficulty you'd anticipate.
Of those choices, I would pick "deploying machinery based on decidable equality to avoid proof irrelevance". And having chosen that, I'm not sure I understand why this is counted as a downside. If anything, I would consider this an upside: if you need to prove that two vectors are equal, showing that the vectors' underlying proofs are equivalent becomes very simple due to decidable equality.
This is a subtle point, and I think it's one that helps to illustrate with an example. In particular, I believe you're alluding to the fact that if you have a definition like this: Inductive value : Type :=
| Value (Vec 2 value)Then Rocq will accept this definition if This is an interesting edge case to be sure, but I'm not sure I would elevate this to the level of "stopper". For starters, you can't write a recursive data type like this in either Cryptol or SAWCore, so this sort of code will never arise as a result of |
Right, sure. That said, at this point redoing everything would be pretty expensive :-(
Well, at this point we already have it... plus we can only reuse things that don't involve
I thought I had preserved all the same ordering as the original implementation, but maybe I didn't. Anyway, my current implementation puts the LSB at the near end. Some of the pieces (e.g. the multiply and divide code) would be fairly gnarly the other way, and others (increment, add, etc.) would at least be messier. That suggests I may have to rearrange some things in the exporter to accommodate it, which I'll grant is unfortunate. I suppose ideally we would want a library that's exactly the same as Cryptol's handling of vectors and bitvectors (complete with index 0 of a bitvector being the MSB, even though that's against everyone else's understanding of what "bit 0" means) ... but since Cryptol doesn't define vectors inductively and (AFAIK) doesn't natively have either cons or snoc, the exporter shouldn't care which end is the near end. Also my first concern is making the Rocq output comprehensible because that's what people doing manual proofs need to look at. Which I think means I should go ahead and switch to snoc.
I'm talking about making the type That is, not encoding the lengths in the types at all.
I'm talking about approaches like the stdlib's which you can probably imagine becomes impractical pretty quickly. Even if you can come up with a good definition of
Yes, assuming you're familiar with those tools. It's still a bunch of goop you need that is deep into prover metatheory and confusing to casual users. In an adequately complete library, users shouldn't need to see it, same as with dealing with the related cases in the dependently-typed inductive. Is it actually any simpler, given adequate internal machinery? I'm not convinced. (To be clear, I expected to have much more trouble with this code than I did. The only thing I couldn't get to go was Because we're still encoding the length in the type, it doesn't mean that we get to drop the explicit length coercions or the headaches they cause. So I don't think it makes any real difference for users of the library, which is who we need to be most concerned about.
Right.
You can't in Cryptol. I thought you could in SAWCore, but apparently not. (Nonetheless, there's been some talk about adding inductives to Cryptol and at some point if we want to be able to prove things about non-cryptographic code we'll need either that or some alternative.)
True, that's not a negative usage of |
All the more reason to tread carefully here! I want to make sure that we won't regret our new design and want to redo it yet again some months (or years) later.
To be clear, I didn't have any of the However, you raise a good point about the future of the stdlib's
If we do pick an length-indexed vector representation, then I don't have a very strong opinion about whether it should be done as a cons list or a snoc list. As you've noted, both choices make certain things easier to define and certain things harder to define. The only real constraint is that we should implement big-endian indexing, as Cryptol does this and being consistent with Cryptol would be the least surprising design. I don't have strong feelings about whether we pick a big-endian cons list or a big-endian snoc list, however.
I think I may have given the wrong impression with what I was proposing. I'm not proposing that we translate a SAWCore function argument of type Record Vec (n : nat) (a : Type) : Type := MkVec
{ vlist : list a; veq : length vlist = n }.And that we would translate SAWCore's
I appreciate you providing an example here, but I'm afraid I'm even more confused than I was before. Are you proposing an illustrating a hypothetical encoding of
I would like to challenge the idea that you need to be an expert in Rocq metatheory in order to use the list-bundled-with-a-proof encoding. We can easily offer combinators for users that hide the lower-level details of decidable equality, just like how we would offer combinators that hide the lower-level details of performing dependent pattern matching on length-indexed vectors. For instance, one of the more useful combinators for working over lists-bundled-with-proofs is: Theorem Vec_f_equal :
forall (n : nat) (a : Type) (l1 l2 : list a)
(eq1 : length l1 = n) (eq2 : length l2 = n),
l1 = l2 ->
MkVec l1 eq1 = MkVec l2 eq2.
Proof.
intros.
subst.
f_equal.
apply (UIP_dec Nat.eq_dec).
Qed.With this, most equality proofs over
One advantage of the lists-bundled-with-proofs encoding is that the stdlib already offers a
I agree that explicit length coercions are still required, but I would challenge the idea that they're as much of a headache as they are in the dependently-typed vector setting. One of the nice properties of the lists-bundled-with-proofs encoding is that the representation ( Lemma append_append_l: forall a n m l
(xs: Vec a n) (ys: Vec a m) (zs: Vec a l) pf,
append (append xs ys) zs =
coerceVec (n + m + l) pf (append xs (append ys zs)).
Proof.
intros.
revert pf.
revert zs ys.
revert m l.
induction xs; intros; simpl.
- rewrite coerceVec_vacuous. auto.
- assert (n + m + l = n + (m + l)) as HN1 by lia.
rewrite IHxs with (pf := HN1).
rewrite ConsVec_coerceVec.
apply coerceVec_irr.
Qed.I'd argue (and hopefully you'll agree) that this proof is somewhat tedious, as it requires appealing to three separate helper lemmas ( Let's compare how this proof would work in a lists-bundled-with-proofs setting. We need only establish one lemma about how Theorem pushCoerceVec :
forall (m n : nat) (a : Type) (pf : m = n) (v : Vec m a),
coerceVec _ _ _ pf v = MkVec (vlist v) (eq_trans (veq v) pf).
Proof.
intros.
subst.
now destruct v.
Qed.With this, proving Theorem append_append_l :
forall (m n p : nat) (a : Type) (vm : Vec m a) (vn : Vec n a) (vp : Vec p a),
append vm (append vn vp) =
coerceVec _ _ _ (eq_sym (Nat.add_assoc m n p)) (append (append vm vn) vp).
Proof.
intros.
destruct vm, vn, vp.
rewrite pushCoerceVec.
simpl.
apply Vec_f_equal.
apply List.app_assoc.
Qed.All of the heavy lifting is done in the final To put this another way: Lean also picks the lists-bundled-with-proofs encoding in its own vector definition. I think it would be worth following suit. |
Fair enough.
Well, right, you're not crazy :-)
ok then.
Well, it has indexing from both ends and we'll need that one way or another. The divider is what convinced me it really ought to be LSB on the near end of the list. It could be written inside-out, but it would be a lot uglier. (The multiplier too, except currently it is incorrect and therefore is not an existence proof of anything substantive.)
No, I think I must have been unclear that I was trying to comment on the entire space of design choices.
You don't want to use the record syntax unless you need to. Inductive Vec (n: nat) (a: Type) : Type :=
| MkVec (xs: list a) (pf: length xs = n): Vec n a.On the plus side, because it's not recursive, Given that I need a custom proof term (and therefore a lemma to keep it short) just to define Also, while in the dependently typed case all the proofs contain only nats, here we also have lists floating about. The advantage of proofs that are entirely about nats and not lists is that we have both (Plus anywhere we have equality of lists we lose UIP in the general case...)
Well, sure. For an adequately complete library, these concerns aren't user-facing, and users shouldn't be writing proofs about the library internals. I already said that.
Well, apart from when you have e.g. one list of length
I admit that this part is appealing. I am near to persuaded, but as I noted above, tinkering a bit with this formulation suggests that even if eliminating coercions is easier, it's going to have a much higher length proof overhead. And it's fairly easy to write an ltac that eliminates coercions using all the available tools, so the fact that they do end up being user-facing isn't actually that compelling an argument. Also we lose the advantage of dropping down to lists if we want snoc, and I think we do. That way bitvectors will display something like comprehensibly and the behavior of append doesn't require lengthy explanation every time. (I also don't think that proving
Erm, I think you mean
Lean's metatheory and dependent typing formulation is all different so I don't think that really means much. Meanwhile, I had an idea: It was not a good idea. |
|
@sauclovian-g and I discussed this synchronously. The short version is that I'm now aligned with the length-indexed vector approach used in this PR. To summarize some of the key points from the longer discussion we had:
|
381e86b to
d8d70b6
Compare
Preliminary. Thanks to Sam Lasser for showing me how to deal with the dependent matching in atWithProof.
From Val Roberts.
The goal of this was to get coerceVec to go away on its own more often; unfortunately, so far in practice it has the opposite effect.
This one computes reliably (it does not match on the proof term) and is, though somewhat ugly to set up, and a little prone to unfolding without being asked and making a mess, seemingly fully workable. Provisional. Also preliminary, Bitvector.v is bound to need at least minor changes and I haven't yet.
d8d70b6 to
27ec9cf
Compare
The existing one has numerous problems.
This one is much larger and more complete, and as an unexpected bonus, despite this it builds much, much faster, I guess because it doesn't use mathcomp.
On the minus side, it's not going to be compatible with old code, let alone old proofs; the internals are completely different and the arguments of the
Vectype are now in the opposite order. (This is unfortunately necessary: it has to beVec a nwith the type first and then the length, because the type needs to be a parameter and the length needs to be an index, and therefore they must appear in that order.) The old library madeVecaDefinitionthat reverses the order; we could conceivably bring that back but it's rarely a good idea to have noise like that.