leanprover / leanprover/lean4

Better tactics for showing that recursive calls are "decreasing"

Open
#2,353 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature P-medium
Dominant language
Lean
Stars
9.2k
Forks
990
Avg merge
1d 17h
Merged PRs (30d)
175

Description

The ideal solution must include forward chaining + linear arithmetic.
Here is an example where the builtin tactic in the core fails:

import Mathlib.Data.List.Defs
import Std.Data.List.Basic

open Option

inductive Val where
  | Int (i : Int)
  | Tag (ps : List (String × Val))

def asInt : Val → Option Int
  | .Int i => some i
  | _      => none

inductive Expr where
  | Int (i : Int)
  | Add (es : List Expr)
  | Tag (ps : List (String × Expr))

def interpret : Expr → Option Val
  | .Int i  => some (Val.Int i)
  | .Add es => do -- works with attach
    let vs ← es.attach.mapM (fun ⟨e, _⟩ => interpret e)
    let is ← vs.mapM asInt
    some (Val.Int is.sum)
  | .Tag ps => do -- doesn't work with attach: termination proof fails
    let vs ← ps.attach.mapM (fun ⟨⟨s, e⟩, _⟩ => do let v ← interpret e; some (s, v))
    some (Val.Tag vs)

Here is the a possible workaround.

import Mathlib.Tactic.Linarith
import Mathlib.Data.List.Defs
import Std.Data.List.Basic

open Option

inductive Val where
  | Int (i : Int)
  | Tag (ps : List (String × Val))

def asInt : Val → Option Int
  | .Int i => some i
  | _      => none

inductive Expr where
  | Int (i : Int)
  | Add (es : List Expr)
  | Tag (ps : List (String × Expr))

def interpret : Expr → Option Val
  | .Int i  => some (Val.Int i)
  | .Add es => do -- works with attach
    let vs ← es.attach.mapM (fun ⟨e, _⟩ => interpret e)
    let is ← vs.mapM asInt
    some (Val.Int is.sum)
  | .Tag ps => do -- doesn't work with attach: termination proof fails
    let vs ← ps.attach.mapM (fun ⟨(s, e), h⟩ => do
      have : sizeOf e < 1 + sizeOf ps := by
        have : sizeOf (s, e) < sizeOf ps := by apply List.sizeOf_lt_of_mem; assumption
        have : sizeOf e < sizeOf (s, e) := by simp
        linarith
      let v ← interpret e; some (s, v))
    some (Val.Tag vs)
/-
By including the `termination_by` we ensure Lean will produce an error message saying where it failed to
prove that call is "decreasing". Then, we learn that it failed to prove that `sizeOf e < 1 + sizeOf ps`.
I "fix" the issue by adding a manual proof. We need better automation in the core for proving this kind of example automatically.
-/
termination_by interpret e => e

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the recursive interpret example, especially the Expr.Tag branch using ps.attach.mapM and termination_by interpret e => e. Start by examining the termination-checking path that handles decreasing recursive calls and the existing forward-chaining or linear-arithmetic support. Done means the original example verifies without the manual sizeOf proof.

Written by the indexing model from the issue text.

Assessment

Domain
compilers
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.