leanprover / leanprover/lean4

'import all' sometimes still requires @[expose] when using grind

Open
#11,948 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Prerequisites
Description

Sometimes @[expose] is required even when using import all to import a module. Even more strangely, this behavior can be affected by function declaration order and whether you also public import that same module.

Consider the following example

Test/Foo.lean

module

public def increment? (i : Nat) (sz: Nat) : Option Nat :=
  if _ : i < sz - 1 then some (i + 1) else none

--@[expose]
public def iterate (a : Array Nat) (i : Nat) : Array Nat :=
  match _ : increment? i a.size with
  | none => a
  | some j =>
    have h: i < a.size := by simp_all[increment?]; omega
    iterate (a.set i a[i]) j
termination_by a.size - i
decreasing_by
  simp_all [increment?]
  omega

Test/Bar.lean

module
import all Test.Foo
public import Test.Foo

theorem iterate_does_nothing (a :Array Nat) (i : Nat) :
    iterate a i = a := by
  induction a, i using iterate.induct with grind [iterate]

With the @[expose] annotation, this will compile. Without it, I get the following error message:

▼ 7:44-7:58: error:
invalid pattern(s) for `iterate.match_1.congr_eq_2`
  [iterate.match_1✝ #5 #4 #3 #2]
the following theorem parameters cannot be instantiated:
  j : Nat
  heq_1 : x✝ = some j

▼ 7:44-7:58: error:
invalid pattern(s) for `iterate.match_1.congr_eq_2`
  [iterate.match_1✝ #5 #4 #3 #2]
the following theorem parameters cannot be instantiated:
  j : Nat
  heq_1 : x✝ = some j

This is already unexpected behavior, but things get even a little stranger.

Consider the following example where we have two copies of iterate, one tagged with @[expose] one not tagged.

Test/Foo.lean

module

public def increment? (i : Nat) (sz: Nat) : Option Nat :=
  if _ : i < sz - 1 then some (i + 1) else none

public def iterate (a : Array Nat) (i : Nat) : Array Nat :=
  match _ : increment? i a.size with
  | none => a
  | some j =>
    have h: i < a.size := by simp_all[increment?]; omega
    iterate (a.set i a[i]) j
termination_by a.size - i
decreasing_by
  simp_all [increment?]
  omega

@[expose]
public def iterate_expose (a : Array Nat) (i : Nat) : Array Nat :=
  match _ : increment? i a.size with
  | none => a
  | some j =>
    have h: i < a.size := by simp_all[increment?]; omega
    iterate_expose (a.set i a[i]) j
termination_by a.size - i
decreasing_by
  simp_all [increment?]
  omega

And the corresponding theorems:

Test/Bar.lean

module
import all Test.Foo
public import Test.Foo

theorem iterate_does_nothing (a :Array Nat) (i : Nat) :
    iterate a i = a := by
  induction a, i using iterate.induct with grind [iterate]

theorem iterate_expose_does_nothing (a :Array Nat) (i : Nat) :
    iterate_expose a i = a := by
  induction a, i using iterate_expose.induct with grind [iterate_expose]

As written, iterate_does_nothing will fail with the aforementioned error message, while iterate_expose_does_nothing will succeed.

However:

  • If public import Test.Foo is removed, both versions will fail. The error message on the exposed version is a more standard grind failure message. Attempting to unfold iterate_expose yields ⊢ iterate_expose._unary ⟨a, i⟩ = a.
  • Even more bizarrely, if you keep public import Test.Foo and swap the declaration order of iterate and iterate_expose in Test/Foo.lean, both theorems succeed.
  • Swapping the definitions and removing public import Test.Foo causes iterate_does_nothing to succeed while iterate_expose_does_nothing does not.
Context

This came up in my PR to add verification to the BinaryHeap implementation in Batteries. leanprover-community/batteries#1602.

Steps to Reproduce

See above.

Expected behavior: import all should not require @[expose] annotations to work, removing public import should not alter whether definitions can be unfolded, declaration order of independent functions should not matter.

Actual behavior: @[expose] annotations are required to make grind prove a theorem even when using import all. Removing public import while having an @[expose] annotation, makes the proof break. Swapping the declaration order of two independent functions makes everything work for some reason.

Versions
Lean 4.28.0-nightly-2026-01-08
Target: x86_64-unknown-linux-gnu

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

Start by reproducing the examples in Test/Foo.lean and Test/Bar.lean with the listed Lean nightly version, comparing import all, public import, declaration order, and @[expose]. Trace the grind proof entry points and unfolding behavior shown by the failures. Done means the examples compile without order-dependent behavior or unnecessary @[expose] annotations, with regression coverage for the reported variants.

Written by the indexing model from the issue text.

Assessment

Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.