Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Delaborators for `IsOpen[t]` etc
Robert Maxton (Nov 09 2025 at 02:21):
So, after being confused one too many times in proofs using non-canonical instances of TopologicalSpace, I decided to make a delaborator that will delaborate into the IsOpen[t] notation when the inferred topology is not what is synthesized:
import Mathlib.Topology.Defs.Basic
open Topology Lean Qq PrettyPrinter.Delaborator SubExpr in
@[app_delab IsOpen]
def delabIsOpen : Delab := whenPPOption Lean.getPPNotation do
match ← inferTypeQ <|<- SubExpr.getExpr with
| ⟨1, ~q(Prop), ~q(@IsOpen $α $τ $s)⟩ => -- fully applied
if let .some τ' ← trySynthInstanceQ q(TopologicalSpace $α) then
if let .defEq _ ← isDefEqQ (α := q(TopologicalSpace $α)) τ τ' then
`($(mkIdent ``IsOpen) $(← PrettyPrinter.delab s))
else
`(IsOpen[$(← PrettyPrinter.delab τ)] $(← PrettyPrinter.delab s))
else
`(IsOpen[$(← PrettyPrinter.delab τ)] $(← PrettyPrinter.delab s))
| ⟨.succ u, ~q(Set $α → Prop), ~q(@IsOpen _ $τ)⟩ => -- partially applied
if let .some τ' ← trySynthInstanceQ q(TopologicalSpace $α) then
if let .defEq _ ← isDefEqQ (α := q(TopologicalSpace $α)) τ τ' then
`($(mkIdent ``IsOpen))
else
`(IsOpen[$(← PrettyPrinter.delab τ)])
else
`(IsOpen[$(← PrettyPrinter.delab τ)])
| _ => failure
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] s ↔ IsOpen[f σ] s := by sorry
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] = IsOpen[f σ] := by sorry
Robert Maxton (Nov 09 2025 at 02:22):
This works, as far as I can tell, but it's clunky, repeating essentially the same code as many as four times in parts. Unfortunately the limited API around LOption and mixing dependently typed functions with monadic contexts make it difficult to merge cases, but I'd like to find out if I'm just missing a nicer solution. I'm also never entirely sure what best practices are with metaprogramming, so I'd appreciate some help golfing + failsafe-ing this code before PR.
Aaron Liu (Nov 09 2025 at 02:36):
oh this looks bad
Robert Maxton (Nov 09 2025 at 02:36):
glad to hear it :v
Aaron Liu (Nov 09 2025 at 02:37):
when you click on the thing in the brackets in the infoview, it shows unrelated hover info
Aaron Liu (Nov 09 2025 at 02:37):
that's why you should use docs#Lean.PrettyPrinter.Delaborator.SubExpr.withNaryArg
Robert Maxton (Nov 09 2025 at 02:38):
Huh. Doesn't do that on my local copy, where I've pasted the above at the bottom of Mathlib.Topology.Defs.Basic itself...
Robert Maxton (Nov 09 2025 at 02:38):
Aaron Liu said:
that's why you should use docs#Lean.PrettyPrinter.Delaborator.SubExpr.withNaryArg
then I can't use Qq and the code turns into an illegible stateful mess :<
Aaron Liu (Nov 09 2025 at 02:38):
Robert Maxton said:
Huh. Doesn't do that on my local copy, where I've pasted the above at the bottom of
Mathlib.Topology.Defs.Basicitself...
did you click in the infoview?
Aaron Liu (Nov 09 2025 at 02:39):
click on the subexpression in the infoview
Robert Maxton (Nov 09 2025 at 02:39):
Yeah, in the infoview.
image.png
Robert Maxton (Nov 09 2025 at 02:39):
Actually it works in the playground too?
Aaron Liu (Nov 09 2025 at 02:40):
click on the subexpression, on the f σ or on the s
Aaron Liu (Nov 09 2025 at 02:40):
you clicked on the whole expression
Robert Maxton (Nov 09 2025 at 02:40):
ah, okay
Aaron Liu (Nov 09 2025 at 02:41):
also, instead of making ident IsOpen you can should just failure -- fail over to the default delaborator
Robert Maxton (Nov 09 2025 at 02:44):
mm.
The other thing is, Qq is clear enough to be self-documenting, the withFoo series really, really isn't
Kenny Lau (Nov 09 2025 at 02:46):
@Robert Maxton
import Mathlib.Topology.Defs.Basic
open Topology Lean Meta PrettyPrinter.Delaborator SubExpr in
@[app_delab IsOpen]
def delabIsOpen : Delab := whenPPOption Lean.getPPNotation do
let space ← withNaryArg 0 getExpr
let instE ← withNaryArg 1 getExpr
let inst ← withNaryArg 1 delab
let .some τ ← Meta.trySynthInstance (← Meta.mkAppM ``TopologicalSpace #[space]) | failure
let set : Option Term ←
if (← getExpr).getAppNumArgs = 3 then
pure <| some <| ← withNaryArg 2 delab
else
pure none
if ← Meta.isDefEq instE τ then
match set with
| none => `($(mkIdent ``IsOpen))
| some set => `($(mkIdent ``IsOpen) $set)
else
match set with
| none => `($(mkIdent ``IsOpen)[$inst])
| some set => `($(mkIdent ``IsOpen)[$inst] $set)
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] s ↔ IsOpen[f σ] s := by sorry
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] = IsOpen[f σ] := by sorry
Robert Maxton (Nov 09 2025 at 02:47):
... Well, I guess if you outright do it for me I can just use that, then :wry:
I'll see if I can understand this well enough to extend it for Continuous[_, _], at least.
Thanks? :p
Kenny Lau (Nov 09 2025 at 02:48):
I must say, this requirement of not fixing the number of arguments is a bit noncanonical
Kenny Lau (Nov 09 2025 at 02:49):
How often do you need to talk about the equality of IsOpen predicate
Robert Maxton (Nov 09 2025 at 02:49):
Almost never, but I want it to work even if you do?
Aaron Liu (Nov 09 2025 at 02:49):
Kenny Lau said:
I must say, this requirement of not fixing the number of arguments is a bit noncanonical
what does this mean?
Kenny Lau (Nov 09 2025 at 02:49):
he wants both IsOpen[t] and IsOpen[t] s to work
Robert Maxton (Nov 09 2025 at 02:50):
It's not quite free (especially since someone else did the work for me this time :p) but it is a small one-time cost and ensures that the notation behaves consistently when partially applied
Robert Maxton (Nov 09 2025 at 02:50):
... Also I take that back, it comes up constantly
Robert Maxton (Nov 09 2025 at 02:50):
every time you prove that a map is inducing or a quotient map, you're proving equality of some topology with (co)induced
Robert Maxton (Nov 09 2025 at 02:52):
... hm, actually, that should go straight to IsOpen[σ] s ↔ IsOpen[τ] s
Robert Maxton (Nov 09 2025 at 02:52):
oh, I think it's le_def I'm thinking of
Robert Maxton (Nov 09 2025 at 02:53):
there's a couple places where you get illegible IsOpen ≤ IsOpens, anyway; that was what motivated trying this at all
Kenny Lau (Nov 09 2025 at 03:02):
@Robert Maxton take 2
import Mathlib.Topology.Defs.Basic
open Topology Lean Meta PrettyPrinter.Delaborator SubExpr in
@[app_delab IsOpen]
def delabIsOpen : Delab := whenPPOption Lean.getPPNotation do
let space ← withNaryArg 0 getExpr
let iE ← withNaryArg 1 getExpr
let inst ← withNaryArg 1 delab
let .some τ ← Meta.trySynthInstance (← Meta.mkAppM ``TopologicalSpace #[space]) | failure
let core ← if ← Meta.isDefEq iE τ then `($(mkIdent ``IsOpen)) else `($(mkIdent ``IsOpen)[$inst])
if (← getExpr).getAppNumArgs = 2 then return core else `($core $(← withNaryArg 2 delab))
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] s ↔ IsOpen[f σ] s := by
guard_target =ₛ IsOpen s ↔ IsOpen[f σ] s
sorry
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] = IsOpen[f σ] := by
guard_target =ₛ IsOpen = IsOpen[f σ]
sorry
Aaron Liu (Nov 09 2025 at 04:25):
Aaron Liu said:
also, instead of making ident
IsOpenyoucanshould justfailure -- fail over to the default delaborator
@Kenny Lau reminder to not mkident isopen
Aaron Liu (Nov 09 2025 at 04:26):
Also you used the wrong syntax
Chris Henson (Nov 09 2025 at 04:42):
Aaron Liu said:
Kenny Lau reminder to not mkident isopen
Can you clarify what you mean and why?
Robert Maxton (Nov 09 2025 at 04:43):
... then `($(mkIdent ``IsOpen)) else can be replaced with ... then failure else
Robert Maxton (Nov 09 2025 at 06:44):
import Mathlib.Topology.Defs.Basic
open Topology Lean Meta PrettyPrinter.Delaborator SubExpr TopologicalSpace
def TopologicalSpace.delabUnary (name : Name) : Delab := whenPPOption Lean.getPPNotation do
let α ← withNaryArg 0 getExpr
let τE ← withNaryArg 1 getExpr
let τ ← withNaryArg 1 delab
let .some τ' ← Meta.trySynthInstance (← Meta.mkAppM ``TopologicalSpace #[α]) | failure
let core ← if ← Meta.isDefEq τE τ' then failure else `($(mkIdent name)[$τ])
if (← getExpr).getAppNumArgs = 1 then return core else `($core $(← withNaryArg 2 delab))
@[app_delab IsOpen] def delabIsOpen : Delab := delabUnary ``IsOpen
@[app_delab IsClosed] def delabIsClosed : Delab := delabUnary ``IsClosed
@[app_delab closure] def delabClosure : Delab := delabUnary ``closure
@[app_delab Continuous]
def TopologicalSpace.delabContinuous : Delab := whenPPOption Lean.getPPNotation do
let α ← withNaryArg 0 getExpr
let β ← withNaryArg 1 getExpr
let τE₁ ← withNaryArg 2 getExpr; let τ₁ ← withNaryArg 2 delab
let τE₂ ← withNaryArg 3 getExpr; let τ₂ ← withNaryArg 3 delab
let .some τ₁' ← Meta.trySynthInstance (← Meta.mkAppM ``TopologicalSpace #[α]) | failure
let .some τ₂' ← Meta.trySynthInstance (← Meta.mkAppM ``TopologicalSpace #[β]) | failure
let core ← if (← Meta.isDefEq τE₁ τ₁') ∧ (← Meta.isDefEq τE₂ τ₂') then failure else
`(Continuous[$τ₁, $τ₂])
if (← getExpr).getAppNumArgs = 2 then return core else `($core $(← withNaryArg 3 delab))
Refactored for better code reuse. However, the fact that I can't write `($(mkIdent name)[$τ₁, $τ₂]) as a general delabBinary builder makes me think I'm doing some kind of abuse-of-syntax here, and somehow getting away with building syntax that's being initially parsed as a getElem and is re-interpreted as a proper IsOpen[_] later.
Eric Wieser (Nov 09 2025 at 11:03):
@Kenny Lau , that still looks wrong to me, I think you want
open Topology Lean Meta PrettyPrinter.Delaborator SubExpr in
@[app_delab IsOpen]
def delabIsOpen : Delab := withOverApp 2 <| whenPPOption Lean.getPPNotation do
let α ← withNaryArg 0 getExpr
let .some synthInst ← Meta.trySynthInstance (← mkAppM ``TopologicalSpace #[α]) | failure
let inst ← withNaryArg 1 getExpr
if ← Meta.isDefEq inst synthInst then failure
let instD ← withNaryArg 1 delab
`(IsOpen[$instD])
Eric Wieser (Nov 09 2025 at 11:04):
Otherwise you're generating GetElem notation on IsOpen
Robert Maxton (Nov 09 2025 at 11:04):
I thought that was what was going on there. It's a delab, so we're getting away with it, but it's probably not exactly best practice.
Eric Wieser (Nov 09 2025 at 11:05):
You're not getting away with it, if you hover over the [ in the infoview in the previous version it says the wrong thing (i.e., not the docstring for IsOpen[ notation)
Kenny Lau (Nov 09 2025 at 11:07):
@Eric Wieser I see, I didn't know about withOverApp and also I wasn't thinking about getelem
Eric Wieser (Nov 09 2025 at 11:07):
Kenny Lau said:
he wants both
IsOpen[t]andIsOpen[t] sto work
Indeed, withOverApp is the answer to this
Kenny Lau (Nov 09 2025 at 11:07):
I instead tried withBoundedAppFn and got an error
Eric Wieser (Nov 09 2025 at 11:08):
@Robert Maxton, could you adjust your continuity delaborator to use the style of my message, and then PR both in a single PR?
Robert Maxton (Nov 09 2025 at 11:09):
... Hm, interesting. I knew about withOverApp, but I usually think of it in the case of "After fully applying this constant, the result may sometimes be a function and therefore might be further applied", which can't be the case here because IsOpen becomes a Prop. But okay, this is a new usage for me!
Eric Wieser (Nov 09 2025 at 11:10):
IsOpen[t] is a function, not a Prop
Kenny Lau (Nov 09 2025 at 11:10):
a function can become prop
Robert Maxton (Nov 09 2025 at 11:10):
yeah, but IsOpen[t] s is a Prop, so that's what I was focusing on
Robert Maxton (Nov 09 2025 at 11:28):
Robert Maxton (Nov 09 2025 at 11:28):
though if it fails CI I'll fix it tomorrow, it's late
Robert Maxton (Nov 09 2025 at 22:41):
Do I need to do anything after making the MathlibTest file? mk_all or the like?
Robert Maxton (Nov 09 2025 at 22:51):
Also, while the delaborator works in examples, it seems to throw 'false negatives' in #check:
import Mathlib.Topology.Defs.Basic
open scoped Topology
variable {α β : Type*} [σ : TopologicalSpace α] [τ : TopologicalSpace β] (f : α → β)
(s : Set α) (t : Set β) (induced : (α → β) → TopologicalSpace β → TopologicalSpace α)
-- Infoview correctly shows `IsOpen t ↔ IsOpen[induced f τ] s`:
-- `τ` is the only instance on β, thus 'canonical'
-- `induced f τ` is not defeq to the instance `σ`, thus 'noncanonical'.
example : IsOpen[τ] t ↔ IsOpen[induced f τ] s := by sorry
-- Printed message is `IsOpen[τ] t`; should just be `IsOpen t`
#check IsOpen[τ] t
Robert Maxton (Nov 10 2025 at 00:11):
mm
/-- Delaborate unary notation referring to non-standard topologies. -/
def delabUnary (mkStx : Term → DelabM Term) : Delab :=
withOverApp 2 <| whenPPOption Lean.getPPNotation do
let α ← withNaryArg 0 getExpr
dbg_trace "delabUnary called for space {← PrettyPrinter.delab α}"
match ← Meta.trySynthInstance (← Meta.mkAppM ``TopologicalSpace #[α]) with
| .none => dbg_trace "delabUnary: no synthInst"; failure
| .undef => dbg_trace "delabUnary: synthInst = undef"; failure
| .some synthInst =>
let inst ← withNaryArg 1 getExpr
dbg_trace "assertNonCanonical: space = {← PrettyPrinter.delab α}, synthInst = {← PrettyPrinter.delab synthInst}, inst = {← PrettyPrinter.delab inst}"
if ← Meta.isDefEq inst synthInst then failure
let instD ← withNaryArg 1 delab
mkStx instD
This version with traces shows that trySynthInstance fails in the #check, but works fine in the examples; is there something we can do about that?
Aaron Liu (Nov 10 2025 at 00:12):
sounds like a possible problem with #check
Robert Maxton (Nov 10 2025 at 00:13):
mm
Meanwhile, guard_target accepts too much:
open scoped Topology in
example {α} [σ : TopologicalSpace α] (f : TopologicalSpace α → TopologicalSpace α) (s : Set α) :
IsOpen[σ] s ↔ IsOpen[f σ] s := by
guard_target =ₛ IsOpen[σ] s ↔ IsOpen[f σ] s
sorry
passes
Aaron Liu (Nov 10 2025 at 00:15):
well guard_target only does a "syntactic" check
Robert Maxton (Nov 10 2025 at 00:17):
right
I was hoping for something like "IsOpen[σ] s is delaborated to IsOpen s and then fails the guard_target for not being syntactically equal to IsOpen[σ] s", but I guess that would be annoying in a different way
Aaron Liu (Nov 10 2025 at 00:18):
yeah "syntactic" equality isn't about syntax
Robert Maxton (Nov 10 2025 at 00:18):
pft
Robert Maxton (Nov 10 2025 at 00:18):
I see
Aaron Liu (Nov 10 2025 at 00:18):
syntax contains way too much information for a direct equality check to be useful
Aaron Liu (Nov 10 2025 at 00:19):
for example, the source position of everything
Robert Maxton (Nov 10 2025 at 00:22):
fair
Robert Maxton (Nov 10 2025 at 00:22):
anyway, is there another way than #check to trace a hypothesis/target for checking?
Eric Wieser (Nov 10 2025 at 00:23):
Does replacing trySynthInstance with synthInstance help?
Eric Wieser (Nov 10 2025 at 00:24):
Alternatively, do trace_state in a proof instead of #check
Robert Maxton (Nov 10 2025 at 00:25):
Eric Wieser said:
Does replacing
trySynthInstancewithsynthInstancehelp?
... sort of, in that it explicitly fails and makes the specific problem obvious
failed to pretty print expression (use 'set_option pp.rawOnError true' for raw representation) : Prop
Robert Maxton (Nov 10 2025 at 00:30):
Eric Wieser said:
Alternatively, do
trace_statein a proof instead of #check
trace_state gives the exact same behavior
Robert Maxton (Nov 10 2025 at 00:31):
it's only the infoview itself that seems to be working properly
Robert Maxton (Nov 10 2025 at 00:35):
/--
trace: α : Type u_1
σ : TopologicalSpace α
s : Set α
⊢ IsOpen s
-/
#guard_msgs(trace) in
example {α : Type*} [σ : TopologicalSpace α] (s : Set α) : IsOpen[σ] s := by
trace_state
sorry
fails because trace_state gives IsOpen[σ] s
Kenny Lau (Nov 10 2025 at 09:40):
Aaron Liu said:
well
guard_targetonly does a "syntactic" check
but #guard_msgs is able to work on the level of strings?
Robert Maxton (Nov 22 2025 at 02:45):
Okay, we seem to have a deeper problem.
import Mathlib.Topology.Order
def discrete {α} : TopologicalSpace α where
IsOpen s := True
isOpen_univ := trivial
isOpen_inter := by simp
isOpen_sUnion := by simp
instance : TopologicalSpace ℕ := discrete
variable (s : Set ℕ) (σ : TopologicalSpace ℕ)
lemma topology_nat : discrete = (inferInstance : TopologicalSpace ℕ) := by rfl
lemma topology_nat' : σ = (inferInstance : TopologicalSpace ℕ) := by rfl
This is deeply surprising behavior to me; σ hasn't even been given instance brackets, but it's automatically become the default TopologicalSpace instance on Nat. Is this known intended behavior somehow? Because I'm halfway through writing a bug report on Lean core otherwise lol
Robert Maxton (Nov 22 2025 at 02:47):
This isn't really a blocker for the original notation per se, though it does complicate writing properly general checks; it also means that the two-topologies test @Kenny Lau asked for is going to give different results than you might predict
Aaron Liu (Nov 22 2025 at 03:18):
That's just kind of how it works
Aaron Liu (Nov 22 2025 at 03:18):
To the best of my knowledge this is intended
Robert Maxton (Nov 22 2025 at 03:22):
Huh, really? But usually, when you define a function, seeing (h : Foo) means you have to provide the instance explicitly, and only [Foo] means that the instance is synthesized...
I guess that's actually the dual behavior, when the definition is used, huh. Still, somehow I had thought that if you didn't use [_], the proof would treat it like a def-without-instance-attribute
Robert Maxton (Nov 22 2025 at 03:22):
Certainly, it seems a bug that it takes priority over registered instances even without instance-implicit binders
Aaron Liu (Nov 22 2025 at 03:28):
Robert Maxton said:
Certainly, it seems a bug that it takes priority over registered instances even without instance-implicit binders
That's probably not a bug too
Aaron Liu (Nov 22 2025 at 03:29):
how about this: when you let : TypeClass Foo := instance that should make it an instance, even though let-binders are always default binderinfo and not inst-implicit
Robert Maxton (Nov 22 2025 at 03:43):
Mm. I feel like that's 'an exception that is required because of limits on let', rather than 'desired canonical behavior'?
like, I would be happy having to write let [_] : TypeClass Foo instead
Aaron Liu (Nov 22 2025 at 03:49):
Also, all those theorems that take {h : Decidable c} for example docs#dif_pos
Robert Maxton (Nov 22 2025 at 03:50):
mmmmm
That's true. It starts to feel like you'd almost want a second 'layer' of notation, so you can specify separately what get synthesized and what gets registered as an instance
Robert Maxton (Nov 22 2025 at 03:50):
I do recall Kyle mentioning once that deciding what should and shouldn't be registered is hard, this is probably what he was talking about :wry:
Robert Maxton (Nov 22 2025 at 03:54):
Well, anyway, I guess I'll just stick with using (co)induced to generate my non-canonical instances then, since variables complicate things
Kevin Buzzard (Nov 23 2025 at 01:55):
I think "I just put two different topologies on a type and now things are weird" is expected behaviour; the point of the typeclass system is "don't put more than one instance on a type", as opposed to "put more than one instance on a type and then expect to know which one typeclass inference will find at any given point". Note that already
instance : TopologicalSpace ℕ := discrete
is bad because docs#instTopologicalSpaceNat is already there (as you can see with #synth TopologicalSpace ℕ -- note also that the idiomatic spelling of discrete is ⊥ : TopologicalSpace X, as was used there). If you want to put more than one instance of a topological space on a type, use a type synonym.
Robert Maxton (Nov 23 2025 at 02:03):
Note that the example does not import Mathlib.Topology.Order, as it's intended for a unit testing file where I would prefer not to import more than the specific file required. In that file Nat has no registered instance yet and the lattice structure on TopologicalSpace a has not yet been defined.
Robert Maxton (Nov 23 2025 at 02:05):
Also, the weirdness would be expected if there were two instances (that were manifestly instances) on a type; the confusion is precisely because they're "just" normal variable definitions/free floating hypotheses.
Kevin Buzzard (Nov 23 2025 at 02:28):
There's no difference in lean 4 (at least as far as I know)
Robert Maxton (Nov 23 2025 at 02:49):
I mean, def Foo and instance Foo are definitely different; it just seems that variable (t : TopologicalSpace a) and variable [TopologicalSpace a] are the same.
Aaron Liu (Nov 23 2025 at 02:51):
well it changes the behavior when you're using the theorems
Aaron Liu (Nov 23 2025 at 02:51):
and it changes the binderinfo
Aaron Liu (Nov 23 2025 at 02:51):
so when you revert it will preserve the binderinfo
Aaron Liu (Nov 23 2025 at 02:51):
but I think they're mostly the same for instance synthesis
Kevin Buzzard (Nov 23 2025 at 13:26):
Yeah exactly, I mean to say that once you're in a proof then everything in the local context is visible to the typeclass inference system.
Last updated: Dec 20 2025 at 21:32 UTC