Skip to content

Commit

Permalink
feat: allow updating binders to and from strict- and instance-implicit (
Browse files Browse the repository at this point in the history
#6634)

This PR adds support for changing the binder annotations of existing
variables to and from strict-implicit and instance-implicit using the
`variable` command.

This PR requires a stage0 update to fully take effect.

Closes #6078
  • Loading branch information
jrr6 authored Feb 5, 2025
1 parent f1ed830 commit 60aeb79
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 101 deletions.
69 changes: 43 additions & 26 deletions src/Lean/Elab/BuiltinCommand.lean
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,20 @@ private partial def elabChoiceAux (cmds : Array Syntax) (i : Nat) : CommandElabM

open Lean.Parser.Term

private def typelessBinder? : Syntax → Option (Array (TSyntax [`ident, `Lean.Parser.Term.hole]) × Bool)
| `(bracketedBinderF|($ids*)) => some (ids, true)
| `(bracketedBinderF|{$ids*}) => some (ids, false)
| _ => none
private def typelessBinder? : Syntax → Option (Array (TSyntax [`ident, `Lean.Parser.Term.hole]) × BinderInfo)
| `(bracketedBinderF|($ids*)) => some (ids, .default)
| `(bracketedBinderF|{$ids*}) => some (ids, .implicit)
| `(bracketedBinderF|⦃$ids*⦄) => some (ids, .strictImplicit)
| `(bracketedBinderF|[$id:ident]) => some (#[id], .instImplicit)
| _ => none

/-- If `id` is an identifier, return true if `ids` contains `id`. -/
private def containsId (ids : Array (TSyntax [`ident, ``Parser.Term.hole])) (id : TSyntax [`ident, ``Parser.Term.hole]) : Bool :=
id.raw.isIdent && ids.any fun id' => id'.raw.getId == id.raw.getId

/--
Auxiliary method for processing binder annotation update commands: `variable (α)` and `variable {α}`.
Auxiliary method for processing binder annotation update commands:
`variable (α)`, `variable {α}`, `variable ⦃α⦄`, and `variable [α]`.
The argument `binder` is the binder of the `variable` command.
The method returns an array containing the "residue", that is, variables that do not correspond to updates.
Recall that a `bracketedBinder` can be of the form `(x y)`.
Expand All @@ -169,31 +172,30 @@ private def containsId (ids : Array (TSyntax [`ident, ``Parser.Term.hole])) (id
The second `variable` command updates the binder annotation for `α`, and returns "residue" `γ`.
-/
private def replaceBinderAnnotation (binder : TSyntax ``Parser.Term.bracketedBinder) : CommandElabM (Array (TSyntax ``Parser.Term.bracketedBinder)) := do
let some (binderIds, explicit) := typelessBinder? binder | return #[binder]
let some (binderIds, binderInfo) := typelessBinder? binder | return #[binder]
let varDecls := (← getScope).varDecls
let mut varDeclsNew := #[]
let mut binderIds := binderIds
let mut binderIdsIniSize := binderIds.size
let mut modifiedVarDecls := false
-- Go through declarations in reverse to respect shadowing
for varDecl in varDecls.reverse do
let (ids, ty?, explicit') ← match varDecl with
let (ids, ty?, binderInfo') ← match varDecl with
| `(bracketedBinderF|($ids* $[: $ty?]? $(annot?)?)) =>
if annot?.isSome then
for binderId in binderIds do
if containsId ids binderId then
throwErrorAt binderId "cannot update binder annotation of variables with default values/tactics"
pure (ids, ty?, true)
pure (ids, ty?, .default)
| `(bracketedBinderF|{$ids* $[: $ty?]?}) =>
pure (ids, ty?, false)
| `(bracketedBinderF|[$id : $_]) =>
for binderId in binderIds do
if binderId.raw.isIdent && binderId.raw.getId == id.getId then
throwErrorAt binderId "cannot change the binder annotation of the previously declared local instance `{id.getId}`"
varDeclsNew := varDeclsNew.push varDecl; continue
pure (ids, ty?, .implicit)
| `(bracketedBinderF|⦃$ids* $[: $ty?]?⦄) =>
pure (ids, ty?, .strictImplicit)
| `(bracketedBinderF|[$id : $ty]) =>
pure (#[⟨id⟩], some ty, .instImplicit)
| _ =>
varDeclsNew := varDeclsNew.push varDecl; continue
if explicit == explicit' then
if binderInfo == binderInfo' then
-- no update, ensure we don't have redundant annotations.
for binderId in binderIds do
if containsId ids binderId then
Expand All @@ -203,26 +205,41 @@ private def replaceBinderAnnotation (binder : TSyntax ``Parser.Term.bracketedBin
-- `binderIds` and `ids` are disjoint
varDeclsNew := varDeclsNew.push varDecl
else
let mkBinder (id : TSyntax [`ident, ``Parser.Term.hole]) (explicit : Bool) : CommandElabM (TSyntax ``Parser.Term.bracketedBinder) :=
if explicit then
`(bracketedBinderF| ($id $[: $ty?]?))
else
`(bracketedBinderF| {$id $[: $ty?]?})
let mkBinder (id : TSyntax [`ident, ``Parser.Term.hole]) (binderInfo : BinderInfo) : CommandElabM (TSyntax ``Parser.Term.bracketedBinder) :=
match binderInfo with
| .default => `(bracketedBinderF| ($id $[: $ty?]?))
| .implicit => `(bracketedBinderF| {$id $[: $ty?]?})
| .strictImplicit => `(bracketedBinderF| {{$id $[: $ty?]?}})
| .instImplicit => do
let some ty := ty?
| throwErrorAt binder "cannot update binder annotation of variable '{id}' to instance implicit:\n\
variable was originally declared without an explicit type"
`(bracketedBinderF| [$(⟨id⟩) : $ty])
for id in ids.reverse do
if let some idx := binderIds.findFinIdx? fun binderId => binderId.raw.isIdent && binderId.raw.getId == id.raw.getId then
binderIds := binderIds.eraseIdx idx
modifiedVarDecls := true
varDeclsNew := varDeclsNew.push (← mkBinder id explicit)
let newBinder ← mkBinder id binderInfo
if binderInfo.isInstImplicit then
-- We elaborate the new binder to make sure it's valid as instance implicit
try
runTermElabM fun _ => Term.withSynthesize <| Term.withAutoBoundImplicit <|
Term.elabBinder newBinder fun _ => pure ()
catch e =>
throwErrorAt binder m!"cannot update binder annotation of variable '{id}' to instance implicit:\n\
{e.toMessageData}"
varDeclsNew := varDeclsNew.push (← mkBinder id binderInfo)
else
varDeclsNew := varDeclsNew.push (← mkBinder id explicit')
varDeclsNew := varDeclsNew.push (← mkBinder id binderInfo')
if modifiedVarDecls then
modifyScope fun scope => { scope with varDecls := varDeclsNew.reverse }
if binderIds.size != binderIdsIniSize then
binderIds.mapM fun binderId =>
if explicit then
`(bracketedBinderF| ($binderId))
else
`(bracketedBinderF| {$binderId})
match binderInfo with
| .default => `(bracketedBinderF| ($binderId))
| .implicit => `(bracketedBinderF| {$binderId})
| .strictImplicit => `(bracketedBinderF| {{$binderId}})
| .instImplicit => throwUnsupportedSyntax
else
return #[binder]

Expand Down
2 changes: 2 additions & 0 deletions stage0/src/stdlib_flags.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "util/options.h"

// update stage0

namespace lean {
options get_default_options() {
options opts;
Expand Down
166 changes: 166 additions & 0 deletions tests/lean/run/varBinderUpdate.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/-!
# Changing variable binder annotations
Tests the use of the `variable` command to update the binder annotations of existing variables. -/

/-! Test updating between default and implicit annotations. -/

namespace Ex1

variable {α : Type}
variable [Add α]
variable (α)
def f (a : α) := a + a
/-- info: f Nat 5 : Nat -/
#guard_msgs in
#check f Nat 5
variable {α}
def g (b : α) := b
/-- info: g 5 : Nat -/
#guard_msgs in
#check g 5
/-- info: Ex1.f (α : Type) [Add α] (a : α) : α -/
#guard_msgs in
#check f
/-- info: Ex1.g {α : Type} (b : α) : α -/
#guard_msgs in
#check g
end Ex1

namespace Ex2

variable {α β : Type}
variable (α)
def f (a : α) := a
def g (b : β) := b
/-- info: f Nat 5 : Nat -/
#guard_msgs in
#check f Nat 5
/-- info: g 5 : Nat -/
#guard_msgs in
#check g 5
/-- info: Ex2.f (α : Type) (a : α) : α -/
#guard_msgs in
#check f
/-- info: Ex2.g {β : Type} (b : β) : β -/
#guard_msgs in
#check g
/-- error: redundant binder annotation update -/
#guard_msgs in
variable (α)
end Ex2

namespace Ex3

variable {α : Type}
variable (f : α → α)
variable (α)
def g (a : α) := f a
/-- info: Ex3.g (α : Type) (f : α → α) (a : α) : α -/
#guard_msgs in
#check g
variable {f}
def h (a : α) := f a
/-- info: Ex3.h (α : Type) {f : α → α} (a : α) : α -/
#guard_msgs in
#check h
end Ex3

namespace Ex4

variable {α β : Type}
variable (α γ)
def g (a : α) (b : β) (c : γ) := (a, b, c)
/-- info: g Nat Bool 10 "hello" true : Nat × String × Bool -/
#guard_msgs in
#check g Nat Bool 10 "hello" true
variable (f : α → α)
variable {f γ α}
def h (a : α) (c : γ) := (f a, c)
/-- info: Ex4.h.{u_1} {α : Type} {γ : Type u_1} {f : α → α} (a : α) (c : γ) : α × γ -/
#guard_msgs in
#check h
end Ex4

/-! Test updating from and to instance implicit. -/

namespace Ex5

variable [i : Add α]
variable (i)
def f (x y : α) := x + y
/-- info: Ex5.f.{u_1} {α : Type u_1} (i : Add α) (x y : α) : α -/
#guard_msgs in
#check f
variable [i]
def g (x y : α) := x + y
/-- info: Ex5.g.{u_1} {α : Type u_1} [i : Add α] (x y : α) : α -/
#guard_msgs in
#check g
end Ex5

/-! Test that variables with default values/tactics cannot be updated. -/

namespace Ex6

variable (a : Nat)
variable (h : a = a := rfl)
/-- error: cannot update binder annotation of variables with default values/tactics -/
#guard_msgs in
variable {h}
/-- error: cannot update binder annotation of variables with default values/tactics -/
#guard_msgs in
variable {a h}
def f := a
/-- info: Ex6.f (a : Nat) : Nat -/
#guard_msgs in
#check f
end Ex6

/-! Test that variables that cannot be instance implicit fail to be updated thereto. -/

namespace Ex7

variable (n : Nat)
/--
error: cannot update binder annotation of variable 'n' to instance implicit:
invalid binder annotation, type is not a class instance
Nat
use the command `set_option checkBinderAnnotations false` to disable the check
-/
#guard_msgs in
variable [n]
variable (x)
/--
error: cannot update binder annotation of variable 'x' to instance implicit:
variable was originally declared without an explicit type
-/
#guard_msgs in
variable [x]
end Ex7

/-! Test updating to and from strict implicit annotations. -/

namespace Ex8

variable {α : Type} (β γ : Type)
variable ⦃α⦄
def f (a : α) (_ : β) := a
/-- info: Ex8.f ⦃α : Type⦄ (β : Type) (a : α) : β → α -/
#guard_msgs in
#check f
variable (α) ⦃β γ⦄
def g (a : α) (b : β) (c : γ) := (a, b, c)
/-- info: Ex8.g (α : Type) ⦃β γ : Type⦄ (a : α) (b : β) (c : γ) : α × β × γ -/
#guard_msgs in
#check g
variable {β}
def h (b : β) := b
/-- info: Ex8.h {β : Type} (b : β) : β -/
#guard_msgs in
#check h
variable {{β}}
/-- error: redundant binder annotation update -/
#guard_msgs in
variable ⦃β⦄
end Ex8
61 changes: 0 additions & 61 deletions tests/lean/varBinderUpdate.lean

This file was deleted.

14 changes: 0 additions & 14 deletions tests/lean/varBinderUpdate.lean.expected.out

This file was deleted.

0 comments on commit 60aeb79

Please sign in to comment.