-
Notifications
You must be signed in to change notification settings - Fork 236
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
Incompatibilities between functional extensionality, subtyping, and untyped equality #1542
Comments
@aseemr @tahina-pro @catalin-hritcu @victor-dumitrescu and I discussed this at length today. There are a few different perspectives. This first note is about typed vs. untyped equality.
This is indeed tricky, but encoding F* equality to SMT equality is essential to making the SMT encoding work in practice at all. Trying to encode some custom notion of equality in SMT is a massive change and unlikely to work. We prefer to take as a hard constraing that That said, even without the SMT solver in the loop, using tactics alone it is possible to exploit equality on functions with subtyping to derive false. For example, taking a variant of one of Aseem's examples:
This is provable without SMT and makes use of that |
Another view is that the treatment of functional extensionality in the presence of subtyping is the culprit. From this perspective, we propose the following revision of the FStar.FunctionalExtensionality library.
Essentially, with this revision, we support functional extensionality only on functions whose domain is suitably restricted. The intended usage is that in contexts that rely on extensionality, we would work with explicitly restricted functions. Note, the crucial bit is to not be able to prove that |
We considered and rejected a proposal to allow proving:
Equivalently, one may see this as revealing that the But, this is doomed. Since one can prove that eta expansions respects provable equality. i.e.,
Which would, in conjunction with |
Overall, in comparison with other proofs of false discovered in F*, this one is particularly pernicious since it's not the result of an implementation bug. Rather, it's a problem in the formulation of a familiar axiom that interacts poorly with the presence of subtyping in the core of the system. Our current plan is to revise the FunctionalExtensionality library as outlined in the 5-step plan above and to evaluate the impact in our existing codebases. |
@aseemr also rightly points out that the same problem also applies to FStar.PredicateExtensionality.
We should apply that same |
More troubles: Looking into predicate extensionality, and then into propositional extensionality on which it is based, I see that the propositional extensionality axiom is inconsistent with the recently revised definition of i.e., we now have
i.e., the type of sub-singletons. However, we do not identify all sub-singletons. For example, the following two types are not equal
Both of these types are However, with the extensionality axiom, if we do have This is already demonstrated in I see three courses of action:
And
|
About propositional extensionality and prop : a candidate for your second option would be the quotient of On the topic of functional extensionality, I have seen it presented sometimes as the combination of congruence under lambda (or weak functional extensionality) (As a side remark, I think we rely crucially on η to prove that the wp-monads satisfy monadic-laws but that's at the meta-theoretical level) |
The propositional extensionality issue is serious too. F* uses any miss-application of the current axiom to immediately prove false: type t1 = | C1
type t2 = | C2
let f () : Lemma (requires (t1 == t2)) (ensures False) = ()
let g () : Lemma (t1 <==> t2) = ()
let done () : Lemma (ensures False) = apply t1 t2 What I'm not yet sure is how exactly is |
Here is a derivation of module Test
#set-options "--no_smt"
type t1 = | C1
type t2 = | C2
let f12 (x : t1) : GTot t2 = match x with | C1 -> C2
let f21 (x : t2) : GTot t1 = match x with | C2 -> C1
let impl12 : (t1 ==> t2) = FStar.Squash.return_squash f12
let impl21 : (t2 ==> t1) = FStar.Squash.return_squash f21
(* I don't understand why F* does not accept the inlined version... *)
let biimpl : (l_and (t1 ==> t2) (t2 ==> t1)) = FStar.Squash.return_squash (And impl12 impl21)
let equiv12 : (t1 <==> t2) = biimpl The The let coerce #a #b (h: a == b) (x :a) : b = x
let f0 (h: t2 == t1) : False = match coerce h C2 with The first one is equality reflection as shown in coerce. It allows in particular to derive The second one is its specific pattern matching. I have trouble to express exactly how pattern-matching is specified, any help would be welcome. My understanding is that this pattern-matching is deemed exhaustive since the only constructor C1 of t1 cannot be matched by the scrutinee. This feature does not seem derivable in more standard type theories (A draft result of Bauer & Winterhalter that I hint to in #1204 shows actually that its negation is consistent with ETT). If I remember correctly the intuitive argument for such a pattern matching relies on canonicity, i.e. that the head of the canonical inhabitants of an inductive in an empty context should be among the list of its constructors. Should that also hold in our context containing the equality |
Another possibility for the definition of
This is closer in spirit to the previous It's also much less disruptive than the other solutions proposed so far, in that it allows us to have propositional and predicate extensionality defined more naturally, as one would expect. And it's compatible with our predominant use of squashed types (unit refinements) as propositions. On the downside, it requires making use of an internalized |
@nikswamy Is it written anywhere what's making I like the spirit of this latest proposal for prop, but (let me be devil's advocate here) what's the argument to explain that it is sound to assume propositional extensionality ? (I don't think we can actually define it, can we ?) Here is my try at it : And if in a context |
So, if we were to define |
My main worry with internalizing |
|
About this comment from Kenji:
[Later update: the following claim about eta is no longer true, since it was unsound] Just to be explicit, η equivalence is still provable in F*.
The The way I see See examples/micro-benchmarks/Test.FunctionalExtensionality.fst (in the |
This is fixed in master based on the plan above. See also Closing the issue, we can still use it for discussions. |
So far I haven't been able to find anything terrible when |
Catching up only now.. sorry for not being around during the heroic effort! I am also pretty convinced this is fixed now, and very well so. For (In any case, I think we cannot have the "singletons" definition of |
That's an interesting variant of prop extensionality. But is squash (squash p) == squash p?
|
Yes, by the same axiom, since |
aseem >>> Our SMT encoding of functional extensionality is currently unsound and allows for a proof of False. Here is an example:
The lemma_g_is_injective is clearly wrong: it says any two int -> int functions that are extensionally equal on nat arguments, are extensionally equal for int arguments.When we encode functional extensionality to Z3, we say forall (x:a). f1 x == f2 x ==> f1 == f2. In F* == has a type argument, and so, equality is only at that type. But when we encode == to Z3, we encode it into native equality and that loses the type information. As a result, we can read back that equality at some other type, as in the example above.Just to complete the proof of False, continuing the example above:
We can discuss it in the F* meeting on Monday.
guido >>> Ouch, this looks nasty
nik >>> ugh
nik >>> this reminds me of the same problem with dropping the type arguments when encoding heterogeneous equality
nik >>> g isn't really necessary, it seems. This works too
nik >>>
let lemma_g_is_injective (f1 f2:int -> int)
:Lemma (requires (feq #nat #int f1 f2))
(ensures (feq #int #int f1 f2)) = ()
guido >>> This is not an encoding issue, afaict, but about reconciling that axiom with subtyping
guido >>> a could be very well be empty (x:int{False}) (edited)
aseem >>> I thought it's an encoding issue since in F* the == has type, which is lost in the smt encoding
guido >>> Yes, but we can eliminate it anyway, no?
aseem >>> Not sure, typing should prevent such cases just in F*
nik >>> I agree, it's not an encoding issue, but a problem with the formulation of the axiom itself
nik >>> I'm considering this alternative: (edited)
nik >>>
assume Extensionality :
forall (a:Type) (b:Type) (f: efun a b) (g: efun a b).
{:pattern feq #a #b f g} feq #a #b f g <==> (fun (x:a) -> f x) == (fun (x:a) -> g x)
nik >>> explicitly restricting the domain of f and g to a
nik >>> but it's rather brittle and somewhat odd to rely on an eta-expansion to restrict the domain in this way
nik >>> this particular trick doesn't work though, because the SMT encoding actively attempts to remove redundant eta-expansions
aseem >>> If we just deep embed eq2 then the axiom fine as it is?
aseem >>> So why is is not an encoding issue?
aseem >>> (I tried that version too!)
aseem >>> in its current form the axiom has eq2 #(a -> b) f g which seems fine to me
nik >>> but eq2 is very primitive ... we encode it everywhere to Z3 =. Changing this to a deep encoding would mess up almost everything, i think
aseem >>> I was thinking for function types (edited)
aseem >>> Essentially I am saying feq is the notion of equality for functions
aseem >>> But that's breaking too, I agree
nik >>> How about something like:
nik >>>
let eta #a (#b:a -> Type) (f:(x:a -> b x)) (x:a) = f x
assume Extensionality :
forall (a:Type) (b:Type) (f: efun a b) (g: efun a b).
{:pattern feq #a #b f g} feq #a #b f g <==> (eta #a f == eta #a g)
nik >>> This time the SMT encoding doesn't eliminate the eta-expansion
nik >>> and this is no longer provable
nik >>>
let lemma_g_is_injective (f1 f2:int -> int)
:Lemma (requires (feq #nat #int f1 f2))
(ensures (feq #int #int f1 f2)) = ()
nik >>> which is good
nik >>> but sadly, neither is
nik >>>
let test (f: (int -> int)) = assert (f == eta f)
nik >>> But, we can prove eta f == eta (eta f)
nik >>> actually, this doesn't really help .. since we can kind of trick the SMT encoding into eliminating the eta anyway
nik >>>
let extensionality (a:Type) (b:Type) (f: efun a b) (g: efun a b)
: Lemma (ensures (feq #a #b f g <==> (eta #a #(fun _ -> b) f == eta #a #(fun _ -> b) g)))
[SMTPat (feq #a #b f g)]
= admit()
let lemma_g_is_injective (f1 f2:int -> int)
:Lemma (requires (feq #nat #int f1 f2))
(ensures (f1 == f2)) =
extensionality nat int f1 f2;
assert (eta #nat #(fun _ -> int) f1 == eta #nat #(fun _ -> int) f2);
assert (eta #nat #(fun _ -> int) f1 == (f1 <: nat -> int))
by (norm [delta_only [
%eta]]); assert (eta #nat #(fun _ -> int) f2 == (f2 <: nat -> int)) by (norm [delta_only [
%eta]])nik >>> hmm, this one is nasty
aseem >>> With function subtyping around, saying eq2 #(a -> b) f g translates to Z3_equal f g seems fishy (edited)
nik >>> agreed
nik >>> alternatives?
aseem >>> We could try to remove this axiom, keep feq as the notion of function equality, and see how much of the code breaks, I guess F* CI would have some cases, may not be as many in mitls and hacl*?
nik >>> it's used quite a lot in many places ...
aseem >>> Yeah ...
nik >>> will have to think more, but so far, I suspect we need some way (like eta above) to restrict the domain of a function in a robust way, i.e., in some way that the SMT encoding doesn't drop it
aseem >>> Yeah, I will also think about it today
aseem >>> How about this:
aseem >>>
assume Extensionality : forall (a:Type) (b:Type) (f: efun a b) (g: efun a b). feq #a #b f g <==> (forall (f1 f2:a -> b) (x:a). (f1 x == f x /\ f2 x == g x) ==> f1 == f2)
(edited)
nik >>> what prevents instantiating f1, f2 with f, g?
nik >>> I was thinking of something like this:
nik >>>
type efun (a:Type) (b:a -> Type) = x:a -> Tot (b x)
type feq #a #b (f:efun a b) (g:efun a b) =
(forall x.{:pattern (f x) / (g x)} f x == g x)
abstract
let restrict #a (#b:a -> Type) (f:(x:a -> b x)) (x:a) = f x
open FStar.Tactics
let extensionality #a #b (f: efun a b) (g: efun a b)
: Lemma (ensures (feq f g <==> restrict f == restrict g))
[SMTPat (feq f g)]
= admit()
let restrict_properties #a #b (f:efun a b)
: Lemma (ensures (feq f (restrict f)))
[SMTPat (restrict f)]
= ()
nik >>> so that you can prove
nik >>>
let test #a #b (f:efun a b) =
assert (restrict f == restrict (restrict f))
(edited)
nik >>> but not
nik >>>
assert (f == restrict f)
nik >>> This will still break stuff, since we'll have to start reasoning about this idempotent restrict thing ...
aseem >>> So we won't be able to prove:
let f1 (x:int) = 2
let f2 (x:int) = 2
assert (f1 == f2)
nik >>> but, at least we can have real == on restricted functions
aseem >>> we can prove restrict f1 == restrict f2
nik >>> yes
nik >>> maybe we could try to figure out some way to support that if you have restrict #t f and if there are there are no types t' such that t <: t' and t' is larger than t, then restrict #t f == f.
nik >>> but that sounds fragile
nik >>> anyway, this restrict thing is the only thing I got right now ...
nik >>> how did you come across this @aseem? it's nasty ... i'm surprised it's been under the radar for so long
aseem >>> Came up in some proof with buffers, where it worked when it shouldn't have :(
nik >>> alright, turning in for the night. talk to you at the meeting
aseem >>> Talk to aseem Yesterday at 10:25 PM
Our SMT encoding of functional extensionality is currently unsound and allows for a proof of False. Here is an example:
module Test
open FStar.FunctionalExtensionality
#set-options "--max_fuel 0 --max_ifuel 0"
let g (f:int -> int) :nat -> int = f
let lemma_g_is_injective (f1 f2:int -> int) :Lemma (requires (feq #nat #int (g f1) (g f2))) (ensures (feq #int #int f1 f2)) = ()
The lemma_g_is_injective is clearly wrong: it says any two int -> int functions that are extensionally equal on nat arguments, are extensionally equal for int arguments.When we encode functional extensionality to Z3, we say forall (x:a). f1 x == f2 x ==> f1 == f2. In F* == has a type argument, and so, equality is only at that type. But when we encode == to Z3, we encode it into native equality and that loses the type information. As a result, we can read back that equality at some other type, as in the example above.Just to complete the proof of False, continuing the example above:
let f1 :int -> int = fun x -> if x >= 0 then 0 else 1
let f2 :int -> int = fun x -> if x >= 0 then 0 else 2
let bad () :squash False = lemma_g_is_injective f1 f2; assert (f1 (-1) == f2 (-1))
We can discuss it in the F* meeting on Monday.
guido >>> Ouch, this looks nasty
nik >>> ugh
nik >>> this reminds me of the same problem with dropping the type arguments when encoding heterogeneous equality
nik >>> g isn't really necessary, it seems. This works too
nik >>>
let lemma_g_is_injective (f1 f2:int -> int)
:Lemma (requires (feq #nat #int f1 f2))
(ensures (feq #int #int f1 f2)) = ()
guido >>> This is not an encoding issue, afaict, but about reconciling that axiom with subtyping
guido >>> a could be very well be empty (x:int{False}) (edited)
aseem >>> I thought it's an encoding issue since in F* the == has type, which is lost in the smt encoding
guido >>> Yes, but we can eliminate it anyway, no?
aseem >>> Not sure, typing should prevent such cases just in F*
nik >>> I agree, it's not an encoding issue, but a problem with the formulation of the axiom itself
nik >>> I'm considering this alternative: (edited)
nik >>>
assume Extensionality :
forall (a:Type) (b:Type) (f: efun a b) (g: efun a b).
{:pattern feq #a #b f g} feq #a #b f g <==> (fun (x:a) -> f x) == (fun (x:a) -> g x)
nik >>> explicitly restricting the domain of f and g to a
nik >>> but it's rather brittle and somewhat odd to rely on an eta-expansion to restrict the domain in this way
nik >>> this particular trick doesn't work though, because the SMT encoding actively attempts to remove redundant eta-expansions
aseem >>> If we just deep embed eq2 then the axiom fine as it is?
aseem >>> So why is is not an encoding issue?
aseem >>> (I tried that version too!)
aseem >>> in its current form the axiom has eq2 #(a -> b) f g which seems fine to me
nik >>> but eq2 is very primitive ... we encode it everywhere to Z3 =. Changing this to a deep encoding would mess up almost everything, i think
aseem >>> I was thinking for function types (edited)
aseem >>> Essentially I am saying feq is the notion of equality for functions
aseem >>> But that's breaking too, I agree
nik >>> How about something like:
nik >>>
let eta #a (#b:a -> Type) (f:(x:a -> b x)) (x:a) = f x
assume Extensionality :
forall (a:Type) (b:Type) (f: efun a b) (g: efun a b).
{:pattern feq #a #b f g} feq #a #b f g <==> (eta #a f == eta #a g)
nik >>> This time the SMT encoding doesn't eliminate the eta-expansion
nik >>> and this is no longer provable
nik >>>
let lemma_g_is_injective (f1 f2:int -> int)
:Lemma (requires (feq #nat #int f1 f2))
(ensures (feq #int #int f1 f2)) = ()
nik >>> which is good
nik >>> but sadly, neither is
nik >>>
let test (f: (int -> int)) = assert (f == eta f)
nik >>> But, we can prove eta f == eta (eta f)
nik >>> actually, this doesn't really help .. since we can kind of trick the SMT encoding into eliminating the eta anyway
nik >>>
let extensionality (a:Type) (b:Type) (f: efun a b) (g: efun a b)
: Lemma (ensures (feq #a #b f g <==> (eta #a #(fun _ -> b) f == eta #a #(fun _ -> b) g)))
[SMTPat (feq #a #b f g)]
= admit()
let lemma_g_is_injective (f1 f2:int -> int)
:Lemma (requires (feq #nat #int f1 f2))
(ensures (f1 == f2)) =
extensionality nat int f1 f2;
assert (eta #nat #(fun _ -> int) f1 == eta #nat #(fun _ -> int) f2);
assert (eta #nat #(fun _ -> int) f1 == (f1 <: nat -> int))
by (norm [delta_only [
%eta]]); assert (eta #nat #(fun _ -> int) f2 == (f2 <: nat -> int)) by (norm [delta_only [
%eta]])nik >>> hmm, this one is nasty
aseem >>> With function subtyping around, saying eq2 #(a -> b) f g translates to Z3_equal f g seems fishy (edited)
nik >>> agreed
nik >>> alternatives?
aseem >>> We could try to remove this axiom, keep feq as the notion of function equality, and see how much of the code breaks, I guess F* CI would have some cases, may not be as many in mitls and hacl*?
nik >>> it's used quite a lot in many places ...
aseem >>> Yeah ...
nik >>> will have to think more, but so far, I suspect we need some way (like eta above) to restrict the domain of a function in a robust way, i.e., in some way that the SMT encoding doesn't drop it
aseem >>> Yeah, I will also think about it today
aseem >>> How about this:
aseem >>>
assume Extensionality : forall (a:Type) (b:Type) (f: efun a b) (g: efun a b). feq #a #b f g <==> (forall (f1 f2:a -> b) (x:a). (f1 x == f x /\ f2 x == g x) ==> f1 == f2)
(edited)
nik >>> what prevents instantiating f1, f2 with f, g?
nik >>> I was thinking of something like this:
nik >>>
type efun (a:Type) (b:a -> Type) = x:a -> Tot (b x)
type feq #a #b (f:efun a b) (g:efun a b) =
(forall x.{:pattern (f x) / (g x)} f x == g x)
abstract
let restrict #a (#b:a -> Type) (f:(x:a -> b x)) (x:a) = f x
open FStar.Tactics
let extensionality #a #b (f: efun a b) (g: efun a b)
: Lemma (ensures (feq f g <==> restrict f == restrict g))
[SMTPat (feq f g)]
= admit()
let restrict_properties #a #b (f:efun a b)
: Lemma (ensures (feq f (restrict f)))
[SMTPat (restrict f)]
= ()
nik >>> so that you can prove
nik >>>
let test #a #b (f:efun a b) =
assert (restrict f == restrict (restrict f))
(edited)
nik >>> but not
nik >>>
assert (f == restrict f)
nik >>> This will still break stuff, since we'll have to start reasoning about this idempotent restrict thing ...
aseem >>> So we won't be able to prove:
let f1 (x:int) = 2
let f2 (x:int) = 2
assert (f1 == f2)
nik >>> but, at least we can have real == on restricted functions
aseem >>> we can prove restrict f1 == restrict f2
nik >>> yes
nik >>> maybe we could try to figure out some way to support that if you have restrict #t f and if there are there are no types t' such that t <: t' and t' is larger than t, then restrict #t f == f.
nik >>> but that sounds fragile
nik >>> anyway, this restrict thing is the only thing I got right now ...
nik >>> how did you come across this @aseem? it's nasty ... i'm surprised it's been under the radar for so long
aseem >>> Came up in some proof with buffers, where it worked when it shouldn't have :(
nik >>> alright, turning in for the night. talk to you at the meeting
aseem >>> Talk to you in the morning!
aseem >>> I can try to assess the impact of this change (edited)
nik >>> that would be useful
aseem >>> More bad news(?):
module Test
#set-options "--max_fuel 0 --max_ifuel 0"
let g (f:int -> int) :nat -> int = f
let foo (f1 f2:int -> int) =
assume (g f1 == g f2);
assert (f1 == f2)
This does not rely on the functional extensionality axiom but rather on F* encoding of g as: forall f. g f = f, where = is the native Z3 equality.So: (a) Is our eta-optimization in the smt encoding sound? (b) Suppose we remove (or restrict) the functional extensionality axiom, then may be g f1 == g f2 won't be provable, so it's fine then?
aseem >>> If we wanted to handle this, we can eta expand function encoding (and not do the optimization)you in the morning!
aseem >>> I can try to assess the impact of this change (edited)
nik >>> that would be useful
aseem >>> More bad news(?):
module Test
#set-options "--max_fuel 0 --max_ifuel 0"
let g (f:int -> int) :nat -> int = f
let foo (f1 f2:int -> int) =
assume (g f1 == g f2);
assert (f1 == f2)
This does not rely on the functional extensionality axiom but rather on F* encoding of g as: forall f. g f = f, where = is the native Z3 equality.So: (a) Is our eta-optimization in the smt encoding sound? (b) Suppose we remove (or restrict) the functional extensionality axiom, then may be g f1 == g f2 won't be provable, so it's fine then?
aseem >>> If we wanted to handle this, we can eta expand function encoding (and not do the optimization)
The text was updated successfully, but these errors were encountered: