Zulip Chat Archive

Stream: lean4

Topic: Simplifying only the first level of `let` bindings


Malcolm Langfield (Jan 14 2024 at 13:45):

Suppose I have the following:

  def f (n : Nat) : Nat :=
    let n₁ := n + 1
    let n₂ := n₁ + 1
    n₂

  example : f 2 = 4 := by
    unfold f
    simp (config := {zeta := False}) only

The goal looks like this after the unfold.

 1 goal
s₀ s₁ : State
fuel : 
 (let n₁ := 2 + 1;
  let n₂ := n₁ + 1;
  n₂) =
  4

How can I zeta-reduce just the first let?

Mario Carneiro (Jan 14 2024 at 14:00):

I don't think there is any tactic that will do this, but you can write your own

Malcolm Langfield (Jan 14 2024 at 14:07):

Thanks for the answer, that sounds promising. Let me ask a different but related question: how can I write a tactic to do this?

I have a beginner's familiarity with tactic writing and stuff in MetaM/TacticM, but it would super helpful to get pointed in the right direction of what goal manipulation functions to use.

Malcolm Langfield (Jan 14 2024 at 14:08):

Or if this info is in the metaprogramming book, where to look!

Mario Carneiro (Jan 14 2024 at 14:10):

I think you can do this using docs#Lean.Core.transform to change the expression and docs#Lean.MVarId.replaceTargetDefEq to apply the replacement to the target

Tomas Skrivan (Jan 14 2024 at 14:32):

Here is a tactic that unfolds a let binding using docs#Lean.Expr.replace

import Lean

open Lean Elab Tactic Conv

syntax (name := let_unfold) " let_unfold " ident : conv

def letUnfold (e : Expr) (id : Name) : Expr :=
  e.replace λ e =>
    if e.isLet && e.letName! = id then
      some (e.letBody!.instantiate1 e.letValue!)
    else
      none

@[tactic let_unfold]
def convLetUnfold : Tactic
| `(conv| let_unfold $id:ident) => do
  ( getMainGoal).withContext do
    let lhs  getLhs

    changeLhs (letUnfold lhs id.getId)
| _ => Lean.Elab.throwUnsupportedSyntax

macro " let_unfold " id:ident : tactic => `(tactic| conv => let_unfold $id)


def f (n : Nat) : Nat :=
  let n₁ := n + 1
  let n₂ := n₁ + 1
  n₂

example : f 2 = 4 := by
  unfold f
  conv =>
    lhs
    let_unfold n₁

Malcolm Langfield (Jan 14 2024 at 15:02):

Oh my gosh, wow! Thanks very much @Tomas Skrivan.

Kyle Miller (Jan 14 2024 at 16:30):

This doesn't do exactly what you asked for, but maybe it's worth considering:

import Mathlib.Tactic

def f (n : Nat) : Nat :=
  let n₁ := n + 1
  let n₂ := n₁ + 1
  n₂

example : f 2 = 4 := by
  unfold f
  lift_lets
  /-
  ⊢ let n₁ := 2 + 1;
    let n₂ := n₁ + 1;
    n₂ = 4
  -/
  intro n₁
  unfold_let n₁
  /-
  ⊢ let n₂ := 2 + 1 + 1;
    n₂ = 4
  -/

Kyle Miller (Jan 14 2024 at 16:46):

@Tomas Skrivan Is docs#Lean.Expr.replace safe to use here? What if the let value contains bound variables? Experimentally it seems to work, so instantiate1 must be updating bvars in the substituted expression, which I never realized it did.

Kyle Miller (Jan 14 2024 at 16:48):

Here's another for zeta reducing outermost lets in an expression.

import Mathlib.Tactic

def f (n : Nat) : Nat :=
  let n₁ := n + 1
  let n₂ := n₁ + 1
  n₂

open Lean Elab Tactic in
/--
Zeta reduce the outermost `let` bindings in an expression.
-/
elab "zeta1" : tactic => liftMetaTactic1 fun g => do
  let ty  g.getType
  let ty'  Core.transform ty
    (pre := fun e =>
      if e.isLet then
        return .done <| e.letBody!.instantiate1 e.letValue!
      else
        return .continue)
  g.replaceTargetDefEq ty'

example : f 2 = 4 := by
  unfold f
  zeta1
  /-
  ⊢ (let n₂ := 2 + 1 + 1; n₂) = 4
  -/

(I used Core.transform rather than Meta.transform because apparently instantiate1 handles bvars better than I thought it did.)

Tomas Skrivan (Jan 14 2024 at 16:51):

Kyle Miller said:

Tomas Skrivan Is docs#Lean.Expr.replace safe to use here? What if the let value contains bound variables? Experimentally it seems to work, so instantiate1 must be updating bvars in the substituted expression, which I never realized it did.

I always get confused by bound variables. I think instantiate1 has to update bvars in the body not the value, no? It should not do anything to bvars in the value.

Kyle Miller (Jan 14 2024 at 16:57):

Consider

def f (n : Nat) : Nat  Nat :=
  let n₁ := n + 1
  let n₂ := n₁ + 1
  fun x => n₂

and doing let_unfold n₂. The value for n₂ is bvar0 + 1, and if you substituted that directly you'd get let n₁ := n + 1; fun x => x + 1, which is incorrect. Instead it seems to see that it's going into one level of bvar depth inside that fun and bumps up the bvar index to actually substitute in bvar1 + 1 so that it refers to n₁.

Tomas Skrivan (Jan 14 2024 at 16:59):

Ohh I missed that!

Kyle Miller (Jan 14 2024 at 17:00):

This appears to be one of the C++-implemented functions. This line calls lift_loose_bvars, which I haven't looked at yet, but I'd guess it's doing this bvar bumping.

Kyle Miller (Jan 14 2024 at 17:02):

This is good to know about instantiate1. I'll put it on my todo list to write a docstring for it!

Kyle Miller (Jan 14 2024 at 20:52):

(@Tomas Skrivan lean4#3183. It'd be nice to get some feedback on the new docstrings. These functions are a little tricky to explain.)


Last updated: May 02 2025 at 03:31 UTC