Zulip Chat Archive

Stream: mathlib4

Topic: Is a `definition_wanted` keyword possible?


Terence Tao (Oct 16 2024 at 16:56):

We have a proof_wanted keyword for theorem statements that we wish to assert without affecting the proof environment; a proof_wanted claim cannot be used to prove other claims. This turned out to be important in an unanticipated use case of the PFR repository, in which a placeholder statement of the form theorem <theorem_name> : 0 = 1 := by sorry was used by an automated theorem prover to "prove" several other statements. Replacing this with proof_wanted <theorem_name> : 0 = 1 or proof_wanted <theorem_name> : (sorry:Prop) removes this exploit.

Is it possible to also have a definition_wanted keyword that is a placeholder for a definition, and allows the definition to be used in notation and in subsequent proof_wanted statements, but cannot be used in actual theorems? The existing technique def <definition name> : <definition type> := sorry has the unintended consequence that downstream theorems using that definition can be artificially solved using tactics such as rfl. Here is a concrete example from (an outdated version of) PFR, discovered by an automated theorem prover:

noncomputable def multiDist {m:} {Ω: Fin m  Type*} (: (i:Fin m)  MeasureSpace (Ω i))
  (X : (i : Fin m)  (Ω i)  G) :  := sorry

@[inherit_doc multiDist] notation3:max "D[" X " ; "  "]" => multiDist  X

lemma multiDist_copy {m:} {Ω : Fin m  Type*} {Ω' : Fin m  Type*} ( : (i : Fin m)  MeasureSpace (Ω i))
    (hΩ': (i : Fin m)  MeasureSpace (Ω' i)) (X : (i : Fin m)  (Ω i)  G) (X' : (i : Fin m)  (Ω' i)  G)
    (hident:  i, IdentDistrib (X i) (X' i) ( i).volume (hΩ' i).volume) :
    D[X ; ] = D[X' ; hΩ'] := by
  rfl

Ideally, I would like instead to write

noncomputable definition_wanted multiDist {m:} {Ω: Fin m  Type*} (: (i:Fin m)  MeasureSpace (Ω i))
  (X : (i : Fin m)  (Ω i)  G) : 

@[inherit_doc multiDist] notation3:max "D[" X " ; "  "]" => multiDist  X

proof_wanted multiDist_copy {m:} {Ω : Fin m  Type*} {Ω' : Fin m  Type*} ( : (i : Fin m)  MeasureSpace (Ω i))
    (hΩ': (i : Fin m)  MeasureSpace (Ω' i)) (X : (i : Fin m)  (Ω i)  G) (X' : (i : Fin m)  (Ω' i)  G)
    (hident:  i, IdentDistrib (X i) (X' i) ( i).volume (hΩ' i).volume) :
    D[X ; ] = D[X' ; hΩ']

but to not permit multiDist (or D[X; hΩ]) to be used in any actual theorem statements or proofs. Is such a syntax even possible in Lean?

Kyle Miller (Oct 16 2024 at 17:05):

One solution might be opaque def, which seals up the definition completely, but, unlike axiom, it still ensures you can't prove falsehoods since it requires that there exist some definition with the type, using the Nonempty typeclass.

In this case, making multiDist be opaque would prevent D[X ; hΩ] = D[X' ; hΩ'] from being solved by rfl.

Eric Wieser (Oct 16 2024 at 18:09):

So opaque def multiDist {m:ℕ} {Ω: Fin m → Type*} /- etc -/ : ℝ := sorry

Eric Wieser (Oct 16 2024 at 18:10):

Though this leaves a trap for an unsuspecting mathematician who fills in the sorry and doesn't understand why they can't prove anything about multiDist (answer: they must delete opaque)

Terence Tao (Oct 16 2024 at 18:42):

So this is why I propose a custom definition_wanted keyword, which could be as simple as an alias for opaque def ... := sorry, but which forces the mathematician filling in the sorry to replace that keyword with an actual def with non-sorry content (and presumably this mathematician would not also supply an opaque modifier).

Gabriel Ebner (Oct 16 2024 at 19:42):

A long time ago I wanted to change sorry so that every sorry is unique, which prevents this class of gotchas entirely. Maybe it's too much of a breaking change to do that in core, but you could always add new syntax for it, analogously to Type* vs. Type.

import Lean
open Lean Meta

elab "sorry*" : term <= expectedType => do
  let id := toExpr ( mkFreshUserName `sorry)
  let fvs := ( ( getLCtx).getFVars.mapM (getFVarLocalDecl ·))
    |>.toList.dropWhile (·.isImplementationDetail)
    |>.toArray.map (·.toExpr)
  let ty := mkForall `h .default ( mkEq id id)
    ( mkForallFVars fvs expectedType)
  return mkAppN ( mkSorry ty (synthetic := false))
    (#[ mkEqRefl id] ++ fvs)

def norm1 (x : Nat) : Nat := sorry*
def norm2 (x : Nat) : Nat := sorry*

example {x y} : norm1 x = norm1 y := rfl -- fails
example {x} : norm1 x = norm2 x := rfl -- fails

Terence Tao (Oct 16 2024 at 19:45):

One could perhaps implement both ideas: an (optional) more "secure" version sorry* of sorry, and make definition_wanted syntactic sugar for opaque def ... := sorry*. (One could also update other tactics such as stop to incorporate this version of sorry, as a precaution, even if I can't directly think of an exploit here.)

Yaël Dillies (Oct 16 2024 at 20:15):

Isn't opaque a bit too stringent? Assume Kevin wants to state that two groups are isomorphic and also state that such an isomorphism commutes with some other operations. I would expect he does something like

definition_wanted iso : G =~* G'

proof_wanted iso_comp_f : iso \comp f = g

But now he has to prove that G =~* G' is nonempty! That goes against the point.

Adam Topaz (Oct 16 2024 at 20:44):

The way proof_wanted works is that it creates an axiom, but inside a block that (by force) does not change the environment. To use a similar approach across multiple declarations like this, one would need to have a whole section that doesn't modify the environment. I don't know if we have anything that can be used to accomplish this. The function that's used for proof_wanted is Lean.withoutModifyingEnv.

Adam Topaz (Oct 16 2024 at 20:45):

one could imagine syntax roughly as follows:

throwaway section

definition_wanted foobar : Nat

axiom Baz : GroupCat

proof_wanted : False

end

Adam Topaz (Oct 16 2024 at 20:46):

But I also don't see how to ensure that definition_wanted and proof_wanted are only used in a throwaway section.

Vincent Beffara (Oct 16 2024 at 21:00):

Wait, wouldn't the point of throwaway section be to use plain old def and theorem inside?

Ruben Van de Velde (Oct 16 2024 at 21:00):

With what as body?

Adam Topaz (Oct 16 2024 at 21:01):

Ruben Van de Velde said:

With what as body?

sorry*

Vincent Beffara (Oct 16 2024 at 21:01):

With sorry if necessary, since there is no danger of spilling out?

Adam Topaz (Oct 16 2024 at 21:02):

yes, if we implement throwaway section, then you can do whatever you want in there.

Adam Topaz (Oct 16 2024 at 21:22):

Maybe it's much easier than I had envisioned:

import Lean
open Lean

elab "throwaway" cmds:command* "catch" : command => withoutModifyingEnv do
  for cmd in cmds do
    Elab.Command.elabCommand cmd

throwaway

axiom A : Nat

catch

Damiano Testa (Oct 16 2024 at 21:42):

I think that this may have some issues: for instance, you cannot define notation among the cmds:

throwaway

notation "ℕ" => Nat
/-- error: unknown identifier '«ℕ»' -/
#guard_msgs in
#check 

catch

Damiano Testa (Oct 16 2024 at 21:43):

Maybe that is not a huge limitation, but "sequential parsing of commands" is not the same as "parsing a file".

Damiano Testa (Oct 16 2024 at 21:44):

(I knew about this, because of that).

Adam Topaz (Oct 16 2024 at 21:52):

I see. And looking at the other thread, there is no easy solution that would make this approach work.

Kyle Miller (Oct 16 2024 at 23:06):

It'd take a core change, but it seems like it would be possible to make a throwaway section ... end command, that has the same effect as Adam's command, but which would allow new syntaxes in the interim.

Terence Tao (Oct 17 2024 at 00:52):

On a slightly different note: it appears that linter.unusedVariables complains when a proof_wanted statement involves variables that are not used in the (non-existent) proof. Is there a way to disable this linter for proof_wanted statements by default?

Kyle Miller (Oct 17 2024 at 01:08):

@Terence Tao Turned out to be doable: batteries#997

Yaël Dillies (Oct 17 2024 at 07:53):

Taking note for the future: Don't open an issue, bother Kyle directly :laughing:

Kyle Miller (Oct 17 2024 at 23:19):

I tried out @Gabriel Ebner's proposal, but it turns out there's a gotcha when it comes to section variables. Likely you want to use sorry for definitions, and in that case sorry will include all section variables into the type of the definition automatically.

I'm experimenting in lean4#5757 with making sorries unique at least in the sense that each sorry that appears in the source is not defeq to any other, so for example (rfl : (sorry : Nat) = sorry) fails to typecheck. The norm1 x = norm1 y example is still a defeq, but at least the norm1 x = norm2 x one fails.

If we had a revert_all tactic, then one could implement Gabriel's sorry* as a macro for by revert_all; sorry

Eric Wieser (Oct 18 2024 at 00:50):

revert_all didn't make sense as a tactic with the pre-4.11.0 inclusion mechanism, right?

Eric Wieser (Oct 18 2024 at 00:51):

Am I now right in saying it is always safe in by blocks, or is it only after theorem statements that the context is frozen?

Kyle Miller (Oct 18 2024 at 00:54):

Just theorems

Kyle Miller (Dec 12 2024 at 01:01):

Update: we added unique sorrys to core Lean @Terence Tao. They will show up in a few weeks in the release candidate for the next version. #rss > Recent Commits to lean4:master @ 💬

It still takes a little bit of care to use it — this is not the context-capturing sorry that Gabriel proposed. Instead, it's a sorry that is different from any other sorry. For example, given

def x :  := sorry
def y :  := sorry

then x and y are not definitionally equal. No need for the opaque trick. (These also have a feature that if the sorry ever shows up in the Infoview, you can "go to definition" on them to see where they came from.)

To use it correctly, rather than

noncomputable def multiDist {m:} {Ω: Fin m  Type*} (: (i:Fin m)  MeasureSpace (Ω i))
  (X : (i : Fin m)  (Ω i)  G) :  := sorry

you would write

noncomputable def multiDist : {m:}  {Ω: Fin m  Type*}  (: (i:Fin m)  MeasureSpace (Ω i)) 
  (X : (i : Fin m)  (Ω i)  G)   := sorry

The first form gives a sorry : ℝ that's unique to multiDist, so the gotcha is multiDist is a constant function. The second form makes multiDist a parameterized family of sorrys, so if the arguments are different the sorry is different, so to speak.

It should be possible to make a definition_wanted macro that does the transformation into the correct form.

Something that might also be helpful is this macro

macro "sorry " args:term:arg+ : term => do
  let ty  args.foldrM (init := ( `(_))) (fun _ ty => `(_  $ty))
  `((sorry : $ty) $args*)

to let you specify what the sorry is supposed to depend on.

For example,

noncomputable def multiDist {m:} {Ω: Fin m  Type*} (: (i:Fin m)  MeasureSpace (Ω i))
  (X : (i : Fin m)  (Ω i)  G) :  := sorry m Ω  X

This is a manual version of Gabriel's sorry*.

Adam Topaz (Dec 12 2024 at 03:41):

Will the last macro also be in core? If not, then we should probably add it to mathlib.

Kim Morrison (Dec 12 2024 at 03:56):

That last macro is not part of lean#5757, no.


Last updated: May 02 2025 at 03:31 UTC