Zulip Chat Archive

Stream: metaprogramming / tactics

Topic: Matching generic Goals with Qq


Francisco Lima (Jul 11 2025 at 15:35):

I am writing a generic theorem solver, like aesop, for a research project, in the process I need to match the type of the main goal to get certain information, through another discussion, in there @Aaron Liu  mentioned Qq, from there I started using it

Now I am in the process of applying diferent basic operations

so I have this piece of code

let 1, ~q(Prop), e  inferTypeQ type
match e with
| ~q($a = $b) => ...
| ~q(True) => ...
| ~q(¬$a) => ...
| _ => ...

now I want to parse goals of the form α → β

but when added it says that the u level is 2 instead of 1, ok I make the selection happen on the match to make it more modular

like this:

match  inferTypeQ type with
| 1, ~q(Prop), ~q($a = $b) => ...
| 1, ~q(Prop), ~q(True) => ...
| 1, ~q(Prop), ~q(¬$a) => ...
| 2, ~q(Type), ~q($a  $b) => ...
| _ => ...

but it says it cannot collapse the universe levels to a singular one big enough for everything.

Is there any better way to make the matching? Or am I missing something crucial that makes it that much easier?

Eric Wieser (Jul 11 2025 at 15:46):

Can you provide a #mwe?

Francisco Lima (Jul 11 2025 at 15:54):

For the working example

import Lean
import Qq

open Lean Elab Tactic Meta
open Qq

syntax (name := endSorry) "sorry?" : tactic

-- number and String are for tracking suggestions and intro names
partial def loop (n := 0) : TacticM (String)  :=
  withMainContext do
    let goal  getMainGoal
    let type  goal.getType
    let 1, ~q(Prop), e  inferTypeQ type | throwError "Don't know how to handle yet"
    match e with
    | ~q($a = $b) =>
      admitGoal goal
      return ""
    | ~q(True) =>
      admitGoal goal
      return ""
    | ~q(¬$a) =>
      admitGoal goal
      return ""
    | _ =>
      admitGoal goal
      return ""

elab_rules : tactic
  | `(tactic| sorry?) => do
    let ref  getRef
    let raw  loop
    -- let text := SuggestionText.string raw
    -- let suggestion : Suggestion := {suggestion := text}
    -- addSuggestion ref suggestion
    return

for the example with error simply change the body

partial def loop (n := 0) : TacticM (String) :=
  withMainContext do
    let goal  getMainGoal
    let type  goal.getType

    match  inferTypeQ type with
    | 1, ~q(Prop), ~q($a = $b) =>
      admitGoal goal
      return ""
    | 1, ~q(Prop), ~q(True) =>
      admitGoal goal
      return ""
    | 1, ~q(Prop), ~q(¬$a) =>
      admitGoal goal
      return ""
    | 2, ~q(Type), ~q($a  $b) =>
      admitGoal goal
      return ""
    | _ =>
      admitGoal goal
      return ""

Francisco Lima (Jul 11 2025 at 15:56):

another thing, eventually loop will be recursive (as should be obvious) but at this point it is not yet

Eric Wieser (Jul 11 2025 at 16:11):

Unfortunately it's pretty hard to match on function types with Qq, because of the universes involved

Eric Wieser (Jul 11 2025 at 16:13):

If you're happy staying in universe 0, this works:

    match  inferTypeQ type with
    | 1, ~q(Prop), ~q($a = $b) => sorry
    | 1, ~q(Prop), ~q(True) => sorry
    | 1, ~q(Prop), ~q(¬$a) => sorry
    | 2, ~q(Type), ~q((_ : ($a : Type))  ($b : Type)) => sorry
    | _ => sorry

Francisco Lima (Jul 11 2025 at 16:18):

It compiles but when using it fails

theorem a_true : α  True := by
  sorry?
  sorry?

this one falls into the default case and not the function type

Eric Wieser (Jul 11 2025 at 16:19):

Right, that's not Type -> Type

Eric Wieser (Jul 11 2025 at 16:19):

That's Type -> Prop

Francisco Lima (Jul 11 2025 at 16:21):

simply changing (_ : ($a : Type)) → ($b : Type) to (_ : ($a : Type)) → ($b : Prop) does not seem to work, at least on the Type universe

Francisco Lima (Jul 11 2025 at 16:22):

And also does not work on the Prop universe, though it parses at least

Francisco Lima (Jul 11 2025 at 16:23):

(_ : ($a : Prop)) → ($b : Prop) also parses but does not work for the matching

Eric Wieser (Jul 11 2025 at 16:23):

mwe?

Francisco Lima (Jul 11 2025 at 16:30):

open Lean Elab Tactic Meta
open Qq

syntax (name := endSorry) "sorry?" : tactic

partial def loop (n := 0) : TacticM (String)  :=
  withMainContext do
    let goal  getMainGoal
    let type  goal.getType
    match  inferTypeQ type with
    | 1, ~q(Prop), ~q($a = $b)  =>
      admitGoal goal
      Lean.logInfo "rfl"
      return ""
    | 1, ~q(Prop), ~q(True)  =>
      admitGoal goal
      Lean.logInfo "apply True.intro"
      return ""
    | 1, ~q(Prop), ~q((_ : ($a : Prop))  ($b : Prop)) =>
      admitGoal goal
      Lean.logInfo "lam Prop → Prop"
      return ""
    | 1, ~q(Prop), ~q((_ : ($a : Type))  ($b : Prop)) =>
      admitGoal goal
      Lean.logInfo "lam Type → Prop"
      return ""
    | 2, ~q(Type), ~q((_ : ($a : Type))  ($b : Type)) =>
      admitGoal goal
      Lean.logInfo "lam Type → Type"
      return ""
    | 1, ~q(Prop), ~q(¬$a)  =>
      admitGoal goal
      Lean.logInfo "Not"
      return ""
    | 1, ~q(Prop), a =>
      admitGoal goal
      Lean.logInfo "default"
      return ""

elab_rules : tactic
  | `(tactic| sorry?) => do
    let ref  getRef
    let raw  loop
    return

-- does not work
theorem a_true : α  True := by
  sorry?

-- works
theorem a_then_a {α : Prop} : α  α := by
  sorry?

-- does not work
theorem a_b_true : α  β  True := by
  sorry?

-- works
theorem a_b_then_a {α β : Prop} : α  β  α := by
  sorry?

-- works
theorem a_b_then_b {α β : Prop} : α  β  β := by
  sorry?

Francisco Lima (Jul 11 2025 at 16:31):

Francisco Lima said:

(_ : ($a : Prop)) → ($b : Prop) also parses but does not work for the matching

Correction, works for Prop -> Prop

Eric Wieser (Jul 11 2025 at 16:32):

The ones that do not work are not about Type, they're about Sort u

Francisco Lima (Jul 11 2025 at 16:32):

also these don't work, I suspect for the same reason

theorem a_b_then_a {α : Prop} : α  β  α := by
  sorry?

theorem a_b_then_b {β : Prop} : α  β  β := by
  sorry?

Eric Wieser (Jul 11 2025 at 16:33):

I recommend adding set_option autoImplicit false

Eric Wieser (Jul 11 2025 at 16:34):

Then the code that causing the trouble will be the bit you write, rather than the bit you don't write

Eric Wieser (Jul 11 2025 at 16:36):

But really the lesson here is that ~q matching is not useful for matching Expr.forallE

Eric Wieser (Jul 11 2025 at 16:36):

Where it excels is for matching nested Expr.apps

Francisco Lima (Jul 11 2025 at 16:39):

Do you know of any other way to match these? I can't even fathom how to march these or

theorem a_a :  α, α   True := by sorry
theorem a_b :  α, α   True := by sorry

Mirek Olšák (Jul 11 2025 at 22:22):

There always is a low-level way (possibly with some whnf, or instantiateMVars)

def Lean.Expr.matchArrow? (e : Expr): Option (Expr × Expr) :=
  match e with
  | .forallE _ a b _ =>
    if b.hasLooseBVars then none
    else (a, b)
  | _ => none

run_meta
  let t1 : Expr := q( α : Type, α  True)
  let t2 : Expr := q( β : Type, β  True)
  if t1.isForall then
    forallBoundedTelescope t1 (some 1) (fun fvars body => do
      logInfo m!"forall {fvars[0]!} : {body}"
      match body.matchArrow? with
      | some (a, b) => logInfo m!"From {a} to {b}"
      | _ => pure ()
    )
  match t2.app2? ``Exists with
  | some (_, p) => lambdaBoundedTelescope p 1 (fun fvars body => do
      logInfo m!"exists {fvars[0]!} : {body}"
      match body.matchArrow? with
      | some (a, b) => logInfo m!"From {a} to {b}"
      | _ => pure ()
    )
  | _ => logInfo "Not exists"

Eric Wieser (Jul 12 2025 at 12:25):

You can match the exists fairly easily with qq

Francisco Lima (Jul 15 2025 at 10:58):

Thanks @Mirek Olšák that works really well and solves most of the problems


Last updated: Dec 20 2025 at 21:32 UTC