Zulip Chat Archive
Stream: lean4
Topic: Acces environment from command macro
Patrick Massot (Aug 05 2022 at 10:54):
If I want to write a user command using macro
, is it possible to access the current environment (at macro execution time, not macro writing time)?
Sebastian Ullrich (Aug 05 2022 at 10:58):
No, that's the point where you should switch to elab
Patrick Massot (Aug 05 2022 at 11:02):
Is there a nice example of doing that?
Sebastian Ullrich (Aug 05 2022 at 11:10):
#find
perhaps? https://github.com/leanprover-community/mathlib4/blob/876d9cc42ccbfea023752e40e6edb7279d9c6b5a/Mathlib/Tactic/Find.lean#L67
There's a lot of other stuff about matching types happening there of course
Sebastian Ullrich (Aug 05 2022 at 11:11):
Depending on what information you need, you probably don't need the liftTermElabM
Patrick Massot (Aug 05 2022 at 11:12):
Thanks!
Patrick Massot (Aug 05 2022 at 12:02):
I'm still stuck because I don't know how to mix macro style and elab style. Say I want my custom namespace command where part of the created name comes from the environment. I can do the easy case where that part is actually hard-coded.
import Lean
open Lean Meta
macro "foo" n:num : command =>
`(namespace $(mkIdent $ Name.str `hard_coded ("bar" ++ toString n.getNat)))
foo 1
def x := 3
end hard_coded.bar1
Now I would like to replace hard_coded in the above macro by the value of
head` taken from the environment at command running time. The naive version is
macro "foo" n:num : command =>
`(namespace $(mkIdent $ Name.str $head ("bar" ++ toString n.getNat)))
which doesn't work at all. Then I tried something like
open Lean Elab in
elab "foo'" n:num : command => do
let env : Environment ← liftMacroM $ pure (← getEnv)
match env.find? `head with
| some decl => `(namespace $(mkIdent $ Name.str $(decl.name) ("bar" ++ toString n.getNat)))
| none => throwError "Failed"
which doesn't make any sense.
Mario Carneiro (Aug 05 2022 at 12:07):
@Patrick Massot What is "head" in this example? Is it a global definition? What does the user side code look like?
Mario Carneiro (Aug 05 2022 at 12:08):
it sounds like you might want to resolve head
at elab time, in which case you would call resolveName
or one of its friends
Patrick Massot (Aug 05 2022 at 12:16):
The user code would be
def head := "no_hard_coded"
foo' 2
end not_hard_coded.bar2
Mario Carneiro (Aug 05 2022 at 12:19):
import Lean
import Mathlib.Util.TermUnsafe
open Lean Meta Elab Command
elab "foo" n:num : command => do
let [(head, _)] ← resolveGlobalName `head | throwError "head not found or overloaded"
let head ← unsafe evalConstCheck Name ``Name head
elabCommand (← `(namespace $(mkIdent $ Name.str head ("bar" ++ toString n.getNat))))
def head : Name := `not_hard_coded
foo 1
def x := 3
end not_hard_coded.bar1
Mario Carneiro (Aug 05 2022 at 12:20):
resolveGlobalName
is not the most convenient function to call, there should be a better one somewhere
Patrick Massot (Aug 05 2022 at 12:21):
Thanks! What needs mathlib here?
Sebastian Ullrich (Aug 05 2022 at 12:21):
The unsafe
term macro
Mario Carneiro (Aug 05 2022 at 12:21):
I just fixed it
Mario Carneiro (Aug 05 2022 at 12:22):
I want to PR that to core, it comes up all the time
Patrick Massot (Aug 05 2022 at 12:24):
I was also missing elabCommand
. I searched for it but couldn't find it.
Mario Carneiro (Aug 05 2022 at 12:26):
as you can see, using elabCommand
makes it easy to turn a macro into an elab
Mario Carneiro (Aug 05 2022 at 12:27):
same thing for elabTactic
or elabTerm
Mac (Aug 05 2022 at 17:55):
Mario Carneiro You should probably be using withMacroExpansion
in your example to make sure the macro stack is setup properly:
elab "foo" n:num : command => do
let [(head, _)] ← resolveGlobalName `head | throwError "head not found or overloaded"
let head ← unsafe evalConstCheck Name ``Name head
let cmd ← `(namespace $(mkIdent $ Name.str head ("bar" ++ toString n.getNat)))
withMacroExpansion (← getRef) cmd <| elabCommand cmd
Mac (Aug 05 2022 at 17:57):
Mario Carneiro said:
I want to PR that to core, it comes up all the time
I think the major problem is that evalConst
does not do the usual unsafe
/implementedBy
split done by most other unsafe code. It unsafety being hidden behind an opaque is always the behavior I want in any of my use cases.
Mario Carneiro (Aug 05 2022 at 18:38):
I'm not sure what you mean. evalConst
should definitely be an unsafe
function, it directly causes unsafety if you call it wrong
Mario Carneiro (Aug 05 2022 at 18:40):
A safe interface for evalConst
would be great; I think it could be an elab
that checks the expected type and synthesizes the expr version to ensure no discrepancy
Mario Carneiro (Aug 05 2022 at 18:42):
In any case, the thing that I want to PR is term-mode unsafe
- evalConst is not specifically the motivator here, but rather the unsafe
/implementedBy
trick that comes up many dozens of times in the lean 4 codebase
Mac (Aug 05 2022 at 18:52):
Mario Carneiro said:
I'm not sure what you mean.
evalConst
should definitely be anunsafe
function, it directly causes unsafety if you call it wrong
Yes, but it should be an the unsafe version should be unsafeEvalConst
with the user evalConst
being an standard implementedBy
wrapper. At least, that is always what I want when I use it.
Mac (Aug 05 2022 at 18:54):
Mario Carneiro said:
In any case, the thing that I want to PR is term-mode
unsafe
- evalConst is not specifically the motivator here, but rather theunsafe
/implementedBy
trick that comes up many dozens of times in the lean 4 codebase
I know, and I agree. However, I think evalConst
is unique in that it is one of the few places the unsafety bleeds into user code in burdensome manner. Most other usages in the Lean core utilize the unsafe
/implementedBy
trick to make this more convenient for the end-user (and not require them write dozens of such tricks themselves).
Mario Carneiro (Aug 05 2022 at 18:55):
Mac said:
Mario Carneiro said:
I'm not sure what you mean.
evalConst
should definitely be anunsafe
function, it directly causes unsafety if you call it wrongYes, but it should be an the unsafe version should be
unsafeEvalConst
with the userevalConst
being an standardimplementedBy
wrapper. At least, that is always what I want when I use it.
What I'm saying is that there can't be a safe version of evalConst
, that would just be unsound
Mario Carneiro (Aug 05 2022 at 18:56):
at least, not with the signature it has
Mario Carneiro (Aug 05 2022 at 18:57):
the safe version, as I said, would have to be part-macro in order to ensure that the expected type actually matches the provided Expr
Mario Carneiro (Aug 05 2022 at 18:58):
even evalConstCheck
is only "slightly less unsafe", because it fundamentally can't check this as a regular function
Mac (Aug 05 2022 at 19:10):
Mario Carneiro said:
that would just be unsound
It would not be unsound, the implementedBy
prohibits that already, it would be memory unsafe, though. But that's fine, one naturally is assuming the value is of the expected type to do the evaluation in most use cases.
Mac (Aug 05 2022 at 19:11):
As your example demonstrate, we generally discard this potentially unsafety in use cases.
Mario Carneiro (Aug 05 2022 at 19:15):
Mac said:
It would not be unsound, the
implementedBy
prohibits that already, it would be memory unsafe, though.
Yes, that's what I mean by unsound. unsafe
isn't just line noise, it has meaning and you shouldn't just be wrapping it with a safe wrapper unless you are preventing the memory safety issue from leaking
Mario Carneiro (Aug 05 2022 at 19:16):
Mac said:
As your example demonstrate, we generally discard this potentially unsafety in use cases.
In use cases, we can confirm that the unsafety is encapsulated. In my example we can see that it is safe because I used Name
and ``Name
as parameters
Mario Carneiro (Aug 05 2022 at 19:17):
so the unsafe
is exactly where it should be in this example
Mac (Aug 05 2022 at 19:28):
Mario Carneiro said:
In use cases, we can confirm that the unsafety is encapsulated. In my example we can see that it is safe because I used
Name
and``Name
as parameters
Note that this is not true, (as mentioned in the documentation for evalConst
):
This function is only safe to use if the type matches the declaration's type in the environment
and if `enableInitializersExecution` has been used before importing any modules
The key here is "if the type matches the declaration's type in the [provided] environment" . In your example, there is no fool-proof guarantee that the type Name
in the environment (which is arbitrary environment accepted at the run time of the command) matches that of the type Name
mentioned at the use site (which is fixed to the Name of compile time elaboration environment). For example, if this elab command was used as a pluign in a prelude
module with a different definition for Lean.Name
, it could cause memory unsafety. Or, if the olean was used with a different Lean version where the name definition changed (as it recently did), it could also cause memory unsafety.
In summary, the safety guarantee for evalConst
is essentially unmeetable. Every practical use of its still introduces the possibility of memory unsafety.
Mario Carneiro (Aug 05 2022 at 19:48):
In your example, there is no fool-proof guarantee that the type Name in the environment (which is arbitrary environment accepted at the run time of the command) matches that of the type Name mentioned at the use site (which is fixed to the Name of compile time elaboration environment).
The environment has to be an extension of the current compile time environment in order to call this function.
Mario Carneiro (Aug 05 2022 at 19:51):
If the oleans don't match, then you are already in UB territory. Lean provides no guarantees in this case
Mac (Aug 05 2022 at 19:52):
Mario Carneiro said:
The environment has to be an extension of the current compile time environment in order to call this function.
No it does not (at least with the use of plugins). Just like one can use (and does use) builtin Lean elaborates without importing all of Lean. Furthermore, even an extension of the environment could modify the meaning of Name
(that would be extremely hacky and probably break other things, but it is still possible).
Mario Carneiro (Aug 05 2022 at 19:52):
Furthermore, even an extension of the environment could modify the meaning of Name (that would be extremely hacky and probably break other things, but it is still possible).
You can't remove or change definitions in the environment, that is something the kernel itself prevents
Mario Carneiro (Aug 05 2022 at 19:55):
As for plugins, I would call that an unsafe
feature. You have to abide by certain rules when using plugins to avoid memory unsafety
Mario Carneiro (Aug 05 2022 at 19:55):
you can get all sorts of link errors that way
Mac (Aug 05 2022 at 19:55):
@Mario Carneiro The environment's constants are just stored in an SMap
, they are perfectly modifiable (though I don't doubt that would cause major problems elsewhere).
Mac (Aug 05 2022 at 20:00):
Actually, based on this comment in SMap
, supporting deletions would not be that hard. So maybe it wouldn't break tons of things elsewhere.
Mario Carneiro (Aug 05 2022 at 20:01):
the comment says that it would require changing the definition of the map to have Option
keys
Mario Carneiro (Aug 05 2022 at 20:01):
in other words, they aren't really being deleted, only hidden from the frontend
Mac (Aug 05 2022 at 20:01):
@Mario Carneiro yes, to properly efficiently support them in the core. You could still overwrite the whole map to do so if you wanted.
Mario Carneiro (Aug 05 2022 at 20:02):
I still wouldn't put the blame for this on evalConst
, those are some unsafe operations you are doing
Mac (Aug 05 2022 at 20:02):
Mario Carneiro said:
in other words, they aren't really being deleted, only hidden from the frontend
That second map is for local declarations, it is then compacted into the first map on a import. So, after an import, I think it would be deleted?
Mario Carneiro (Aug 05 2022 at 20:04):
it's not implemented so it's difficult to speculate on what exactly it would do
Mario Carneiro (Aug 05 2022 at 20:05):
like, you can also just add things to the environment without calling the kernel too
Mario Carneiro (Aug 05 2022 at 20:06):
or you could override the toplevel to play tetris instead
Mario Carneiro (Aug 05 2022 at 20:06):
at some point you aren't really using lean anymore
Mario Carneiro (Aug 05 2022 at 20:07):
and if you get into trouble it's on you
Mac (Aug 05 2022 at 20:07):
Mario Carneiro said:
I still wouldn't put the blame for this on
evalConst
, those are some unsafe operations you are doing
Since you didn't like my reasonable (imo) example of plugins and my hacky example of an environment modifications, here is another one: running the elaborator on a external environment. For instance, if we are in the process of elaborating an external file (ala runFrontend
) and wish to execute this elaborator (or a set of elaborators in general including this one) on some synthetic syntax related to the file (and thus in its environment) the eval would also break.
Mario Carneiro (Aug 05 2022 at 20:09):
This command and many others like it do not support that use case
Mario Carneiro (Aug 05 2022 at 20:09):
if you try to use it anyway that's on you
Mario Carneiro (Aug 05 2022 at 20:10):
unsafe
is a way of saying "don't come to me if your hacks don't work"
Mac (Aug 05 2022 at 20:10):
UB in certain use cases is generally what "unsafe" means.
Mario Carneiro (Aug 05 2022 at 20:10):
importModules
is unsafe
for this reason
Mac (Aug 05 2022 at 20:11):
importModules
is not unsafe
?
Mario Carneiro (Aug 05 2022 at 20:11):
withImportModules
Mac (Aug 05 2022 at 20:11):
which is not generally used directly when augmenting the frontend?
Mario Carneiro (Aug 05 2022 at 20:13):
lean isn't very bulletproof esp. if you try writing your own prelude
Mario Carneiro (Aug 05 2022 at 20:14):
time being finite and all that
Mario Carneiro (Aug 05 2022 at 20:15):
but the notion of what is unsafe
or not for the purpose of "blame assignment" is fairly well trodden in e.g. rust, and lean is trying to follow the same patterns
Mac (Aug 05 2022 at 20:16):
Regardless, my general point, is that by bypassing the unsafe in your example, what the user is stating that they don't care about the edge cases where the behavior may still be unsafe, not that they have reasonably verified that any such case is now impossible. Which is fine, but is an important distinction in guarentees.
Mac (Aug 05 2022 at 20:23):
In that vein, I am not sure what the unsafe
adds in that code, it is essentially silently introducing an equivalent to the common unsafe
/implementedBy
trick for every use case of the evalConst*
without preforming additional verification to ensure it is less unsafe. I don't understand how that would be more useful than just do that trick at the definition of evalConst
(or evalConstCheck
) since that is generally always the way it will be used.
That isn't to say I think the term level unsafe
is useless in general (it is a great idea that removes a lot of burdensome boilerplate). I just think the evalConst
is one place where the unsafe should be hidden away and mentioned in the docstring, as it will be just be hidden away at all its use sites otherwise.
Mac (Aug 05 2022 at 20:26):
Though, I guess that last comment is more relevant now without the term-level unsafe in the Lean core, than once it is there. Because currently hiding it away takes a lot of boilerplate. With the unsafe
macro, it just takes one keyword (which admittedly does nicely documented to the read of the code that the function being used is introducing unsafety).
Mario Carneiro (Aug 05 2022 at 20:47):
Mac said:
In that vein, I am not sure what the
unsafe
adds in that code, it is essentially silently introducing an equivalent to the commonunsafe
/implementedBy
trick for every use case of theevalConst*
without preforming additional verification to ensure it is less unsafe.
This is rust-style verification: you read the code carefully and convince yourself that it is correct. The unsafe
alerts you to the possibility that something that requires additional scrutiny is going on. Lean-style verification would have proof side conditions and for most memory safety issues in lean that's not an option because the side conditions cannot be expressed in the lean logic.
I don't understand how that would be more useful than just do that trick at the definition of
evalConst
(orevalConstCheck
) since that is generally always the way it will be used.
Of course, if the trick could be done in the definition of evalConst
it should be placed there, but the point is that it can't be because the type is generic in evalConst
and it is not generic in the user code.
Mario Carneiro (Aug 05 2022 at 20:51):
Like I said, I think it can be done in general but you would need to use a macro to automate the matching of type to expr, or possibly a typeclass like the reflect
typeclass that did it in lean 3
Mac (Aug 05 2022 at 21:16):
Mario Carneiro said:
Like I said, I think it can be done in general
Assuming that you are okay disregarding cases like plugins, elaborators, and environment changes that I mentioned above. One reason I point this out is that this is the case for Lake which uses evalConst
extensively in such a scenario (on a separately elaborated environment -- that of the configuration file).
Mac (Aug 05 2022 at 21:17):
Mario Carneiro said:
or possibly a typeclass like the
reflect
typeclass that did it in lean 3
You could use the toTypeExpr
of the ToExpr
class for this (I have done this is some places).
Mac (Aug 05 2022 at 21:18):
Though, for cases like this, it might be best to separate toTypeExpr
from ToExpr
, since there are many cases where a type of a value is reflectable even if the value is not.
Mac (Aug 05 2022 at 21:24):
Mario Carneiro said:
Of course, if the trick could be done in the definition of
evalConst
it should be placed there, but the point is that it can't be because the type is generic inevalConst
and it is not generic in the user code.
Huh, yes it can? The following definitions compile just fine:
@[implementedBy Lean.Environment.evalConst] opaque evalConst'
(α) (env : @& Environment) (opts : @& Options) (type : @& Name) : Except String α
@[implementedBy Lean.Environment.evalConstCheck] opaque evalConstCheck'
(α) (env : Environment) (opts : Options) (type : Name) (const : Name) : ExceptT String Id α
Mario Carneiro (Aug 06 2022 at 00:26):
That's not made it any safer though, so taking the unsafe
off is not correct
Mario Carneiro (Aug 06 2022 at 00:27):
if you can make it safer then you can take the unsafe
off
Gabriel Ebner (Aug 06 2022 at 07:50):
Mac said:
Mario Carneiro said:
or possibly a typeclass like the
reflect
typeclass that did it in lean 3You could use the
toTypeExpr
of theToExpr
class for this (I have done this is some places).
This is just as unsafe as calling evalConst directly, since you can safely construct any (unlawful) ToExpr instance.
Sebastian Ullrich (Aug 06 2022 at 08:09):
Mac said:
One reason I point this out is that this is the case for Lake which uses
evalConst
extensively in such a scenario (on a separately elaborated environment -- that of the configuration file).
This is a interesting case though since you're probably not concerned about alternative implementations of those imports in Lake. So you could check whether the imports are resolved to the olean files you expect them to.
Sebastian Ullrich (Aug 06 2022 at 08:12):
For the general case I don't see a better safe solution than comparing deep hashes of the type's structure. It's the same problem as how to detect .olean structure incompatibilities.
Last updated: Dec 20 2023 at 11:08 UTC