Zulip Chat Archive

Stream: mathlib4

Topic: Should we merge `Quotient.out` and `Quotient.unquot`?


Eric Wieser (Sep 01 2023 at 15:17):

We currently have docs#Quot.unquot and docs#Quotient.out.

In Lean3, we needed both because the first was unsound while the second was non-computable; but it seems that the definition of "unsound" (previously meta, now unsafe) has changed in Lean 4, since I can now combine the two as:

import Mathlib.Data.Setoid.Basic
import Mathlib.Data.Multiset.Basic

set_option autoImplicit false

#check Classical.indefiniteDescription

instance {α} {s : Setoid α} (q : Quotient s) : Nonempty ({a : α // a = q}) :=
Classical.indefiniteDescription _ (Quotient.exists_rep q)⟩

def Quotient.indefiniteDescription {α} {s : Setoid α} (q : Quotient s) : {a : α // a = q} :=
unsafe q.unquot, lcProof

/-- A combination of `Quotient.out` and `Quotient.unquot` -/
def Quotient.outC {α} {s : Setoid α} (q : Quotient s) : α := q.indefiniteDescription

lemma Quotient.outC_mk {α} {s : Setoid α} (q : Quotient s) : q.outC = q := q.indefiniteDescription.2

Eric Wieser (Sep 01 2023 at 15:18):

Is this equivalent to attribute [implemented_by Quot.unquot] Quotient.out? If so, is this change sensible?

Mario Carneiro (Sep 01 2023 at 15:19):

no, I think this is unsound

Mario Carneiro (Sep 01 2023 at 15:20):

unquot was and still is unsafe

Eric Wieser (Sep 01 2023 at 15:21):

Is unsafe not synonymous with unsound? Because outC above is not unsafe IIUC

Mario Carneiro (Sep 01 2023 at 15:21):

just because you can wrap an unsafe function in a safe wrapper doesn't mean you have actually discharged the safety condition

Eric Wieser (Sep 01 2023 at 15:21):

What exactly does unsafe mean here?

Mario Carneiro (Sep 01 2023 at 15:21):

"unsound" is what happens when you make a safe function which has unsafe preconditions

Mario Carneiro (Sep 01 2023 at 15:22):

unsafe is a syntactic marker on functions with unsafe preconditions

Eric Wieser (Sep 01 2023 at 15:22):

The context here is this comment on github, where @Scott Morrison suggested I hide the unsafeness of Quot.unquot

Mario Carneiro (Sep 01 2023 at 15:22):

you can't

Mario Carneiro (Sep 01 2023 at 15:23):

at least not with that type signature

Mario Carneiro (Sep 01 2023 at 15:23):

Quotient.out and Quotient.unquot are different functions, they return different values

Mario Carneiro (Sep 01 2023 at 15:24):

one of them isn't even really a function, it returns values that are impossible by the logic

Mario Carneiro (Sep 01 2023 at 15:24):

which is why it is unsafe

Eric Wieser (Sep 01 2023 at 15:24):

I guess I don't see what the point of contagious unsafe is if you can just hide it again

Eric Wieser (Sep 01 2023 at 15:25):

Are you only supposed to use it if you pinky promise that the contents are genuinely sound and lean is just confused?

Mario Carneiro (Sep 01 2023 at 15:25):

unsafe means you need a proof obligation to use the function, only this proof obligation is not expressible in the lean logic

Mario Carneiro (Sep 01 2023 at 15:26):

so you have to do it on paper / "pinky promise" that you have satisfied it

Eric Wieser (Sep 01 2023 at 15:26):

In which case I should revert the silencing of unsafe from #6367?

Mario Carneiro (Sep 01 2023 at 15:27):

yes, the instance needs to be unsafe as it is order dependent even though it has inputs that are not

Eric Wieser (Sep 01 2023 at 15:27):

Mario Carneiro said:

Quotient.out and Quotient.unquot are different functions, they return different values

My proposal is that we delete the former and redefine it as the latter. I agree they are different functions, but within Lean's logic nothing can tell the difference, right?

Mario Carneiro (Sep 01 2023 at 15:28):

incorrect

Kyle Miller (Sep 01 2023 at 15:28):

This doesn't seem good:

abbrev X := Trunc Nat

def a : X := 1
def b : X := 2

#eval a.outC = b.outC
-- false

example : a.out = b.out := by
  rw [Subsingleton.elim a b]

Eric Wieser (Sep 01 2023 at 15:28):

Yes, I think this amounts to implemented_by being allowed to diverge from whatever the logic says

Mario Carneiro (Sep 01 2023 at 15:28):

more specifically we are causing divergence between compiler behavior and model behavior

Mario Carneiro (Sep 01 2023 at 15:28):

and this is not okay

Mario Carneiro (Sep 01 2023 at 15:29):

because the compiler optimizes assuming that the logic is true, so divergence leads to UB

Eric Wieser (Sep 01 2023 at 15:29):

Are there example uses of term unsafe that are ok?

Mario Carneiro (Sep 01 2023 at 15:30):

term unsafe can be used correctly, the idea is that while the general function being called has unsafe preconditions, this particular term combines that function with other things such that the composite satisfies the requisite safety conditions

Mario Carneiro (Sep 01 2023 at 15:31):

If those safety conditions depend on some property of the caller, then you have to propagate the unsafe

Eric Wieser (Sep 01 2023 at 15:32):

Eric Wieser said:

but within Lean's logic nothing can tell the difference, right?

What I mean't here I guess is "within the model you can't tell the difference". As Kyle points out, within the runtime you certainly can. I wasn't aware that divergence could cause UB in the compiler though, so I think that resolves my misunderstanding.

Eric Wieser (Sep 01 2023 at 15:33):

Mario Carneiro said:

yes, the instance needs to be unsafe as it is order dependent even though it has inputs that are not

I've reverted the commit

Mario Carneiro (Sep 01 2023 at 15:33):

The trick is to use that kind of function to use an unchecked array access function like Array.get

Jannis Limperg (Sep 01 2023 at 15:35):

A nice example is docs#ptrEq vs docs#withPtrEq. The former allows you to distinguish equal objects, which is bad. The latter makes you prove that you won't do that, so it can be safe.

Mario Carneiro (Sep 01 2023 at 15:38):

Here's how you can weaponize Kyle's example:

abbrev X := Trunc Nat

def a : X := 1
def b : X := 2

@[noinline] def foo (h : a.outC  b.outC) : Nat := False.elim <| by
  apply h
  rw [Subsingleton.elim a b]

#eval if h : a.outC = b.outC then 1 else foo h

Mario Carneiro (Sep 01 2023 at 15:39):

if you let foo be inlined, it will return 1 because the if statement is optimized away using the fact that False.elim is unreachable

Yuyang Zhao (Sep 09 2023 at 12:35):

I made something similar for docs#Quotient.choice 1.5 months ago: code / Gabriel's comment

Yuyang Zhao (Sep 09 2023 at 15:44):

Is the following code also harmful?

private unsafe def Quotient.choiceImpl {ι : Type*} {α : ι  Type*} [S :  i, Setoid (α i)] :
    ( i, Quotient (S i))  @Quotient ( i, α i) (by infer_instance) :=
  unsafeCast

def Quotient.choice'Aux {ι : Type*} {α : ι  Type*} [S :  i, Setoid (α i)]
    (f :  i, Quotient (S i)) :
     q : @Quotient ( i, α i) (by infer_instance),  i, q.out i = f i :=
  Quotient.choice f, fun i  Quotient.induction_on_pi f
    (fun a  by rw [choice_eq, eq]; exact @Quotient.mk_out _ piSetoid _ _)⟩

@[implemented_by choiceImpl]
def Quotient.choice' {ι : Type*} {α : ι  Type*} [S :  i, Setoid (α i)]
    (f :  i, Quotient (S i)) :
    @Quotient ( i, α i) (by infer_instance) :=
  (Quotient.choice'Aux f).choose

Yuyang Zhao (Sep 09 2023 at 16:18):

After nesting a Classical.choose, it should lose its internal structure now.

Mario Carneiro (Sep 10 2023 at 18:47):

@Yuyang Zhao The exact same counterexample given by Gabriel/Chris above works on the revised version:

def exoticInstance : Decidable True :=
  @Quot.recOnSubsingleton _ _ (fun _ => Decidable True) _
    (@Quotient.choice' (@Quotient Bool trueSetoid) (fun _ => Bool) (fun _ => trueSetoid) id)
    (fun f => decidable_of_iff (f false = f true)
      (iff_true_intro (congr_arg f (Quotient.sound trivial))))

-- VM disagrees with type theory:
example : @decide True exoticInstance = true  := .trans (by congr) decide_True
example : @decide True exoticInstance = false := by native_decide

#eval exoticInstance -- isFalse _

Mario Carneiro (Sep 10 2023 at 18:49):

The basic issue is that (∀ i, Quotient (S i)) -> Quotient (∀ i, α i), while natural-looking, is not sound because it allows you to lift the identity function Trunc A -> Trunc A to Trunc (Trunc A -> A), and even under a Trunc the contained function Trunc A -> A has "impossible properties"

Yuyang Zhao (Sep 10 2023 at 19:31):

I got it. A quotient has different VM representations and they are sent to different VM representations by the function. With unsafeCast, the function will go to different values at the same equivalence class.

Yuyang Zhao (Sep 10 2023 at 19:35):

So in this case it is not possible to write a computable Quotient.choice.


Last updated: Dec 20 2023 at 11:08 UTC