Zulip Chat Archive
Stream: lean4
Topic: Weird Error with Custom Nat Lits
Mac (Apr 18 2021 at 23:37):
So I was defining a Sort
-polymorphic instead of Type
-polymorphic nat lit and ran into the following weird error:
class OfNatLit (A : Sort u) (n : Nat) :=
ofNatLit : A
@[defaultInstance low]
instance {A : Type u} {n : Nat} [K : OfNat A n] : OfNatLit A n
:= {ofNatLit := K.ofNat}
instance {A : Type u} {n : Nat} [K : OfNatLit A n] : OfNat A n
:= {ofNat := K.ofNatLit}
abbrev Zero (A : Sort u) := OfNatLit A (natLit! 0)
-- Before Syntax ovveride: Works as expected on Type
def fooT {A : Type u} [Zero A] (f : A -> Prop) := f 0
-- Before Syntax ovveride: Errors as expected on Sort
def fooS {A : Sort u} [Zero A] (f : A -> Prop) := f 0
/-
invalid universe level, ?u.136 is not greater than 0
-/
syntax (name := sortNumLit) (priority := default + 1) num : term
macro_rules [sortNumLit]
| `( $n:numLit) => `(OfNatLit.ofNatLit (natLit! $n))
-- After syntax override: Errors unexpectedly w/o type scription
def fooS' {A : Sort u} [Zero A] (f : A -> Prop) := f 0
/-
invalid universe level, ?u.525 is not greater than 0
-/
-- After syntax override: But works as expected w/ type ascription
def fooS'' {A : Sort u} [Zero A] (f : A -> Prop) := f (0 : A)
Why does 0
with a type ascription work, but 0
without one error? This seems like a bug in Lean.
Mario Carneiro (Apr 19 2021 at 01:11):
btw natLit!
has been renamed to nat_lit
Mario Carneiro (Apr 19 2021 at 01:27):
It seems like the updated syntax macro is not triggering. I tried this:
#eval show TermElabM Syntax from do
liftMacroM <| expandMacros (← `(term| f 0)) -- unknown parser term
#eval show TermElabM Syntax from do
liftMacroM <| expandMacros (← `(f 0)) -- (Term.app `f._@._hyg.1 [(numLit "0")])
which seems like there isn't any macro expansion happening, but I think I'm not doing it right. @Sebastian Ullrich help!
Sebastian Ullrich (Apr 19 2021 at 07:45):
So as you see in Mario's output (or, even better in general, using pp.raw
and trace.Elab
options), there is no sortNumLit
syntax node to begin with, i.e. the problem is the parser and not the macro. So why would 0
be parsed differently in f 0
and in f (0 : A)
? The only sensible answer is precedences: syntax
will default to a precedence of lead
(which is lower than the "acceptable as an argument" precedence arg
) unless the syntax is "atomic-like", which is necessarily an incomplete heuristic.
Sebastian Ullrich (Apr 19 2021 at 07:56):
We could add all *Lit
kinds to that heuristic, sure. On the other hand, there isn't really a point in introducing a syntax that is equivalent to an existing one, instead we can add a new macro for the existing syntax. Now, not sure if you already tried that, but it does turn out that there are some limitations with macro_rules
that currently make that harder than it should be.
-- fails
macro_rules [numLit]
| `($n:numLit) => `(OfNatLit.ofNatLit (nat_lit $n))
-- succeeds, but registers macro for the wrong syntax kind
macro_rules
| `($n:numLit) => `(OfNatLit.ofNatLit (nat_lit $n))
-- the hard way, then
@[macro numLit] def expandNumLit' : Lean.Macro
| `($n:numLit) => `(OfNatLit.ofNatLit (nat_lit $n))
| _ => Lean.Macro.throwUnsupported
Mario Carneiro (Apr 19 2021 at 08:10):
Do you know why the `(term| ...)
syntax doesn't work @Sebastian Ullrich ?
Mario Carneiro (Apr 19 2021 at 08:11):
What syntax category is f 0
in if not term
?
Sebastian Ullrich (Apr 19 2021 at 09:01):
The named quotations are currently hard-coded. There isn't one for term
since it would be redundant.
Sebastian Ullrich (Apr 19 2021 at 09:01):
Generalizing the syntax yields some "interesting" implementation issues with cyclically dependent subsystems
Mac (Apr 19 2021 at 18:22):
Sebastian Ullrich said:
On the other hand, there isn't really a point in introducing a syntax that is equivalent to an existing one, instead we can add a new macro for the existing syntax.
Unfortunately, I did not know you could add macros to existing syntax kinds like that. However, looking at it now, there was a reason why I made it a separate syntax. Namely, as far as I am aware, you can't scope macro rules. I really wanted the following (I just eliminated the namespaces in my original example to make a #mwe):
namespace Test
scoped syntax (name := sortNumLit) (priority := default + 1) num : term
macro_rules [sortNumLit]
| `( $n:numLit) => `(OfNatLit.ofNatLit (nat_lit $n))
end Test
open Test
However, the following does also work:
namespace Test
@[scoped macro numLit] def expandNumLit : Lean.Macro
| `($n:numLit) => `(OfNatLit.ofNatLit (nat_lit $n))
| _ => Lean.Macro.throwUnsupported
end Test
-- Does not work as expected
def fooS {A : Sort u} [Zero A] (f : A -> Prop) := f 0
open Test
-- Works as expected
def fooS' {A : Sort u} [Zero A] (f : A -> Prop) := f 0
So, thanks!
Sebastian Ullrich (Apr 19 2021 at 19:13):
Yes, if we want to go that way, we should add local/scoped macro(_rules)
Mac (Apr 23 2021 at 22:32):
Sebastian Ullrich said:
On the other hand, there isn't really a point in introducing a syntax that is equivalent to an existing one, instead we can add a new macro for the existing syntax.
I have discovered another good reason to make it new syntax. Overriding numLit
breaks the syntax
command:
syntax:max (name := newNumLit) num : term
@[macro newNumLit]
def expandNewNumLit : Lean.Macro
| n => `(OfNat.ofNat (nat_lit $n))
-- Works fine
syntax "foo" : term
@[macro numLit]
def expandNumLit : Lean.Macro
| n => `(OfNat.ofNat (nat_lit $n))
-- Now breaks
syntax "bar" : term
/-
maximum recursion depth has been reached (use `set_option maxRecDepth <num>` to increase limit)
-/
Mac (Apr 24 2021 at 01:01):
Though I suspect this may be a bug that needs fixing.
Leonardo de Moura (Apr 24 2021 at 20:55):
The macro
@[macro numLit]
def expandNumLit : Lean.Macro
| n => `(OfNat.ofNat (nat_lit $n))
triggers nontermination in any term containing a numeral. The elaborator interrupts the "infinite loop" using the maximum recursion depth threshold.
For example, suppose Lean tries to elaborate the numeral 2
. Your macro expands it to
(OfNat.ofNat (nat_lit 2))
but 2
is a subterm of this term, and your macro is applied again, and so on.
Sebastian Ullrich (Apr 24 2021 at 21:43):
#check 2
works for me, which makes sense because 2
is not a term
in nat_lit 2
. The issue seems to be the attribute argument @[termParser 1024]
, because we eagerly unfold attribute arguments using expandMacros
.
Sebastian Ullrich (Apr 24 2021 at 21:49):
Thus a simple workaround would be to make the macro an elaborator... but then Lean
has to be imported
Leonardo de Moura (Apr 24 2021 at 22:47):
Yes, the macro only causes non-termination if another elaborator or macro invokes expandMacros
For example, the match
-elaborator uses expandMacros
on patterns
@[macro numLit] def expandNumLit : Lean.Macro
| n => `(OfNat.ofNat (nat_lit $n))
def f (x : Nat) : Bool :=
match x with -- non-termination
| 0 => true
| _ => false
Here is a minimal example that triggers.
macro "tst" x:num : term => Lean.expandMacros x
@[macro numLit] def expandNumLit : Lean.Macro
| n => `(OfNat.ofNat (nat_lit $n))
#check tst 2 -- non-termination
That being said, the problem is the expandNumLit
macro that produces a term containing the input term.
Leonardo de Moura (Apr 24 2021 at 22:59):
#check 2 works for me, which makes sense because 2 is not a term in nat_lit 2
@Sebastian Ullrich 2
is a subterm of nat_lit 2
. #check 2
doesn't loop because the nat_lit
elaborator does not invoke elabTerm
or expandMacros
on the subterm 2
.
The nat_lit
elaborator is
@[builtinTermElab rawNatLit] def elabRawNatLit : TermElab := fun stx expectedType? => do
match stx[1].isNatLit? with
| some val => return mkNatLit val
| none => throwIllFormedSyntax
Mario Carneiro (Apr 25 2021 at 03:14):
Sebastian Ullrich said:
Thus a simple workaround would be to make the macro an elaborator... but then
Lean
has to be imported
What is the reason that import Lean
should be avoided? Back in lean 3 days this would basically always be true, and I've just been doing that whenever it is needed. Is there some hidden performance cost of this?
Sebastian Ullrich (Apr 25 2021 at 08:05):
There is no hidden cost, but Lean
is simply very big compared to Init
, so we are at least concerned about importing performance (and maybe performance of other declaration search parts like completion). Right now import Lean
increases import time from 38ms to 380ms for me, which is insignificant for a single file... though keep in mind that all downstream modules will inherit that overhead. All of that would be resolved with https://github.com/leanprover/lean4/issues/416... if we can in fact pull it off.
Mario Carneiro (Apr 25 2021 at 08:15):
I assume that in files where you actually care about the cost to downstream file imports, you can use a more selective import like import Lean.Elab
or what have you? Or is everything mutually dependent enough that this has little advantage?
Mario Carneiro (Apr 25 2021 at 08:16):
(Coming from lean 3 where import times of 5-10 seconds are not unusual, I'm not particularly put off by a 380ms import time :P )
Mac (Apr 25 2021 at 08:31):
In my experience, there is significant noticeable lag when adding a Lean
import when editing in VS Code. I also feel like the response time of the editor decreases somewhat (though not significantly). Though I suspect if most complex metaprogramming requires the whole of Lean to be imported, I imagine most libraries will end up doing so. Thus, I doubt we can really get away from it unless some form of noncumulative/selective importing is introduced (such as the aforementioned module system).
Sebastian Ullrich (Apr 25 2021 at 08:43):
If you register your complex tactics as built-in tactics and compile them down to shared libraries (which you might want to do anyway), you can load them as plugins without actually importing their code, or their dependencies. This is the one way we imagined for "escaping" from import Lean
without the module system.
Sebastian Ullrich (Apr 25 2021 at 08:45):
Mario Carneiro said:
I assume that in files where you actually care about the cost to downstream file imports, you can use a more selective import like
import Lean.Elab
or what have you? Or is everything mutually dependent enough that this has little advantage?
There aren't too many mutual dependencies between subsystems, but at least with Lean.Elab
you do get almost everything (but the language server implementation).
Sebastian Ullrich (Apr 25 2021 at 08:46):
IOW, import Lean.Elab
is ~310ms
Mario Carneiro (Apr 25 2021 at 08:57):
Sebastian Ullrich said:
If you register your complex tactics as built-in tactics and compile them down to shared libraries (which you might want to do anyway), you can load them as plugins without actually importing their code, or their dependencies. This is the one way we imagined for "escaping" from
import Lean
without the module system.
I guess that mathlib will not be able to escape import Lean
then, because even with the mathlib prelude handling the majority of tactics we will still want to write small interactive tactics (that we don't mind being interpreted) inside mathlib itself. What about some kind of "header only" way to import files? I guess you could say that lean 3 did that since most substantial tactics were written in C++ and exposed as constant
to lean.
Mario Carneiro (Apr 25 2021 at 09:04):
I see now that the linked module system issue is also trying to solve this problem, although I want to point out that the requirements of improving import BigDependency
inside the server mode, say for proof authoring, are slightly different from separate compilation, and perhaps some problems that arise in the pursuit of separate compilation aren't as big of a deal when simply making it possible to write interpreted tactics calling lean builtins.
Sebastian Ullrich (Apr 25 2021 at 13:59):
builtinTactic
is such a header system in absence of a true module system. It allows you to use builtin tactics in macros without importing them. If you need a procedural tactic, you have to import at least Lean.Elab.Tactic.Basic
anyway (which currently clocks in at ~170ms), in which case you can use evalTactic
together with a syntax quotation of the builtin tactic to call it, still without actually importing it.
Sebastian Ullrich (Apr 25 2021 at 14:09):
Mario Carneiro said:
although I want to point out that the requirements of improving
import BigDependency
inside the server mode
I should probably clarify that I don't see improving server mode as a concern for the module system, since imports are fast enough for that anyway as we said above. In fact, I could imagine that we would want to lift some import restriction in server mode so that we can still #eval
arbitrary things and e.g. show completion items that would automatically insert necessary additional imports (though ideally in a way that code outside from #eval
would still only be accepted if it was also accepted in batch mode; that part might be a bit tricky). If libraries like mathlib want to embrace the module system, the main benefit would still be separate compilation: rebuilding as few files of it as possible on (most) changes.
Last updated: Dec 20 2023 at 11:08 UTC