Zulip Chat Archive

Stream: lean4

Topic: hygiene question?


Scott Morrison (Dec 02 2022 at 18:00):

I'm trying to write a thin wrapper around aesop, that always includes a particular rule set:

declare_aesop_rule_sets [CategoryTheory]

/-- A thin wrapper for `aesop`, which adds the `CategoryTheory` rule set. -/
macro (name := aesop_cat) "aesop_cat" c:Aesop.tactic_clause*: tactic =>
  `(tactic| aesop $c* (rule_sets [CategoryTheory]))

However with set_option trace.Elab.step true, I can see that aesop_cat is expanding to aesop (rule_sets [CategoryTheory✝]), i.e. CategoryTheory is not referring to the "real" one.

How do I refer to the one I've just declared?

Gabriel Ebner (Dec 02 2022 at 18:06):

That's normal. The CategoryTheory✝ means that it can refer to a local variable bound inside the quotation in addition to the global name.

Scott Morrison (Dec 02 2022 at 18:08):

#mwe:

import Aesop

declare_aesop_rule_sets [ExtraRules]

/-- A thin wrapper for `aesop`, which adds the `ExtraRules` rule set. -/
macro (name := aesop_extra) "aesop_extra" c:Aesop.tactic_clause*: tactic =>
  `(tactic| aesop $c* (rule_sets [ExtraRules]))

-- We turn on `funext` inside `aesop_extra`.
attribute [aesop safe apply (rule_sets [ExtraRules])] funext

example {f g : α  β} (w :  a, f a = g a) : f = g := by
  -- Check `aesop` can't do it.
  aesop
  sorry

example {f g : α  β} (w :  a, f a = g a) : f = g := by aesop (add safe apply funext)

example {f g : α  β} (w :  a, f a = g a) : f = g := by aesop (rule_sets [ExtraRules])

example {f g : α  β} (w :  a, f a = g a) : f = g := by
  aesop_extra
  -- Should be solved now, but isn't.
  sorry

Scott Morrison (Dec 02 2022 at 18:15):

set_option hygiene false in works, but I feel dirty. :-)

Sebastian Ullrich (Dec 02 2022 at 18:40):

If ExtraRules is not an actual declaration name in the environment, Aesop is probably missing an eraseMacroScopes somewhere

Scott Morrison (Dec 02 2022 at 18:59):

@Jannis Limperg, pinging to see if you know where this might be.

Jannis Limperg (Dec 02 2022 at 21:03):

I can eraseMacroScopes wherever I handle rule set names, but at least in principle it could be valid to have a hygienic rule set name (e.g. coming from a macro that defines a rule set). Alternatively, you can explicitly circumvent hygiene from within the macro:

macro (name := aesop_extra) "aesop_extra" c:Aesop.tactic_clause*: tactic =>
  `(tactic| aesop $c* (rule_sets [$(Lean.mkIdent `ExtraRules):ident]))

Sebastian Ullrich (Dec 02 2022 at 21:10):

The hygiene system needs to be aware of scopes to function correctly. The only global scopes it is aware of are the environment and section variables.

Wojciech Nawrocki (Jan 14 2023 at 03:27):

Jannis Limperg said:

I can eraseMacroScopes wherever I handle rule set names, but at least in principle it could be valid to have a hygienic rule set name (e.g. coming from a macro that defines a rule set). Alternatively, you can explicitly circumvent hygiene from within the macro:

macro (name := aesop_extra) "aesop_extra" c:Aesop.tactic_clause*: tactic =>
  `(tactic| aesop $c* (rule_sets [$(Lean.mkIdent `ExtraRules):ident]))

(Ignore me if this has been resolved, although on newest mathlib4 the hygiene false still seems to be needed.)

What Gabriel is saying above, I think, is that hygienic names such as Foo✝ refer both to Foo and Foo✝. This is so that we can write macros like this one

`(
  def foo := 3
  def bar : Nat := foo)

which will elaborate to

def foo := 3
def bar : Nat := foo

where foo✝ will refer to the value introduced by the macro, but Nat✝ will refer to the real Nat. The transformation Name ↦ Name✝ essentially makes all inaccessible names introduced in the current macro scope accessible; but it shouldn't make global names inaccessible. Thus the burden falls on all tactic code to eraseMacroScopes when resolving names..

at least in principle it could be valid to have a hygienic rule set name (e.g. coming from a macro that defines a rule set)

..but this is a valid point. Here is an example to demonstrate the issue.

elab "#resolve" n:ident : command => do
  let nm  resolveGlobalConstWithInfos n
  logInfo m!"resolve: {nm}"

elab "#resolveEMS" n:ident : command => do
  let nm  resolveGlobalConstWithInfos (mkIdentFrom n n.getId.eraseMacroScopes)
  logInfo m!"resolveEMS: {nm}"

elab "#test" : command => do
  let stx  `(
    def foo := 3
    def bar : Nat := foo
    #resolve bar
    #resolveEMS bar)
  elabCommand stx

/- resolve: [bar._@.Mathlib.CategoryTheory.Category.Basic._hyg.661] -/
/- (from resolveEMS) unknown constant 'bar' -/
#test

Does this means that tactics should always try to resolve both n and n.eraseMacroScopes? I am not sure if there is a canonical way to do it. (CC @Sebastian Ullrich)

Sebastian Ullrich (Jan 16 2023 at 10:03):

Wojciech Nawrocki said:

What Gabriel is saying above, I think, is that hygienic names such as Foo✝ refer both to Foo and Foo✝

Whether that is true depends on the exact scopes stored in the identifier. If Foo was in the global scope at the point of the syntax quotation, any identifier Foo inside of the quotation will get a corresponding global scope tag and can resolve to it. If Foo was not in the scope at the point of declaration, then any reference will not and should not refer to it, no. So a tactic should use eraseMacroScopes iff an identifier is not meant to be a Lean declaration reference.

The capture of scopes in quoted identifiers is a non-extensible part of the quotation elaborator, though we recently moved a little towards extensibility by acknowledging that different kinds of scopes exist.

Sebastian Ullrich (Jan 16 2023 at 10:04):

Long story short, the simplest way to ensure that rule set references are hygienic would be to make rule sets actual Lean declarations. That doesn't necessarily mean that their information must be stored in the declarations too, they could simply be stubs.


Last updated: Dec 20 2023 at 11:08 UTC