lean4-htt/src/Lean/Hygiene.lean
Sebastian Ullrich 51bba5338a
perf: make macro scope numbering less dependent on surrounding context (#10027)
This PR changes macro scope numbering from per-module to per-command,
ensuring that unrelated changes to other commands do not affect macro
scopes generated by a command, which improves `prefer_native` hit rates
on bootstrapping as well as avoids further rebuilds under the module
system.

In detail, instead of always using the current module name as a macro
scope prefix, each command now introduces a new macro scope prefix
(called "context") of the shape `<main module>._hygCtx_<uniq>` where
`uniq` is a `UInt32` derived from the command but automatically
incremented in case of conflicts (which must be local to the current
module). In the current implementation, `uniq` is the hash of the
declaration name, if any, or else the hash of the full command's syntax.
Thus, it is always independent of syntactic changes to other commands
(except in case of hash conflicts, which should only happen in practice
for syntactically identical commands) and, in the case of declarations,
also independent of syntactic changes to any private parts of the
declaration.
2025-08-22 13:16:02 +00:00

119 lines
4.6 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Ullrich
-/
module
prelude
public import Lean.Data.Name
public import Lean.Data.Options
public import Lean.Data.Format
public section
namespace Lean
/-! Remark: `MonadQuotation` class is part of the `Init` package and loaded by default since it is used in the builtin command `macro`. -/
structure Unhygienic.Context where
ref : Syntax
scope : MacroScope
/--
Simplistic MonadQuotation that does not guarantee globally fresh names, that
is, between different runs of this or other MonadQuotation implementations.
It is only safe if the syntax quotations do not introduce bindings around
antiquotations, and if references to globals are prefixed with `_root_.`
(which is not allowed to refer to a local variable)
`Unhygienic` can also be seen as a model implementation of `MonadQuotation`
(since it is completely hygienic as long as it is "run" only once and can
assume that there are no other implementations in use, as is the case for the
elaboration monads that carry their macro scope state through the entire
processing of a file). It uses the state monad to query and allocate the
next macro scope, and uses the reader monad to store the stack of scopes
corresponding to `withFreshMacroScope` calls.
-/
abbrev Unhygienic := ReaderT Lean.Unhygienic.Context <| StateM MacroScope
namespace Unhygienic
instance : MonadQuotation Unhygienic where
getRef := return (← read).ref
withRef := fun ref => withReader ({ · with ref := ref })
getCurrMacroScope := return (← read).scope
getContext := pure `UnhygienicMain
withFreshMacroScope := fun x => do
let fresh ← modifyGet fun n => (n, n + 1)
withReader ({ · with scope := fresh}) x
@[inline]
protected def run {α : Type} (x : Unhygienic α) : α := (x ⟨Syntax.missing, firstFrontendMacroScope⟩).run' (firstFrontendMacroScope+1)
end Unhygienic
private def mkInaccessibleUserNameAux (unicode : Bool) (name : Name) (idx : Nat) : Name :=
if unicode then
if idx == 0 then
name.appendAfter "✝"
else
name.appendAfter ("✝" ++ idx.toSuperscriptString)
else
name ++ Name.num `_inaccessible idx
private def mkInaccessibleUserName (unicode : Bool) : Name → Name
| .num p@(.str ..) idx =>
mkInaccessibleUserNameAux unicode p idx
| .num .anonymous idx =>
mkInaccessibleUserNameAux unicode Name.anonymous idx
| .num p idx =>
if unicode then
(mkInaccessibleUserName unicode p).appendAfter ("⁻" ++ idx.toSuperscriptString)
else
Name.mkNum (mkInaccessibleUserName unicode p) idx
| n => n
register_builtin_option pp.sanitizeNames : Bool := {
defValue := true
group := "pp"
descr := "add suffix to shadowed/inaccessible variables when pretty printing"
}
def getSanitizeNames (o : Options) : Bool := pp.sanitizeNames.get o
structure NameSanitizerState where
options : Options
/-- `x` ~> 2 if we're already using `x✝`, `x✝¹` -/
nameStem2Idx : NameMap Nat := {}
/-- `x._hyg...` ~> `x✝` -/
userName2Sanitized : NameMap Name := {}
private partial def mkFreshInaccessibleUserName (userName : Name) (idx : Nat) : StateM NameSanitizerState Name := do
let s ← get
let userNameNew := mkInaccessibleUserName (Std.Format.getUnicode s.options) (Name.mkNum userName idx)
if s.nameStem2Idx.contains userNameNew then
mkFreshInaccessibleUserName userName (idx+1)
else do
modify fun s => { s with nameStem2Idx := s.nameStem2Idx.insert userName (idx+1) }
pure userNameNew
/-- Erase macro scopes from `userName` and add "tombstone" + superscript (or `._hyg`). -/
def sanitizeName (userName : Name) : StateM NameSanitizerState Name := do
let stem := userName.eraseMacroScopes;
let idx := (← get).nameStem2Idx.find? stem |>.getD 0
let san ← mkFreshInaccessibleUserName stem idx
modify fun s => { s with userName2Sanitized := s.userName2Sanitized.insert userName san }
pure san
private partial def sanitizeSyntaxAux : Syntax → StateM NameSanitizerState Syntax
| stx@(Syntax.ident _ _ n _) => do
let n ← match (← get).userName2Sanitized.find? n with
| some n' => pure n'
| none => if n.hasMacroScopes then sanitizeName n else pure n
return mkIdentFrom stx n
| Syntax.node info k args => Syntax.node info k <$> args.mapM sanitizeSyntaxAux
| stx => pure stx
def sanitizeSyntax (stx : Syntax) : StateM NameSanitizerState Syntax := do
if getSanitizeNames (← get).options then
sanitizeSyntaxAux stx
else
pure stx
end Lean