fix: local syntax should create private definitions

This commit is contained in:
Sebastian Ullrich 2025-07-31 18:21:53 +02:00 committed by Cameron Zwarich
parent 8d34dfe914
commit d4a5a2c632
14 changed files with 113 additions and 92 deletions

View file

@ -20,11 +20,11 @@ Declares an auxiliary definition with an automatically generated name.
For example, `aux_def foo : Nat := 42` creates a definition
with an internal, unused name based on the suggestion `foo`.
-/
scoped syntax (name := aux_def) docComment ? attributes ? "aux_def" ident+ ":" term ":=" term : command
scoped syntax (name := aux_def) docComment ? attributes ? visibility "aux_def" ident+ ":" term ":=" term : command
@[builtin_command_elab «aux_def»]
def elabAuxDef : CommandElab
| `($[$doc?:docComment]? $[$attrs?:attributes]? aux_def $suggestion* : $ty := $body) => do
| `($[$doc?:docComment]? $[$attrs?:attributes]? $vis:visibility aux_def $suggestion* : $ty := $body) => do
let id := suggestion.map (·.getId.eraseMacroScopes) |>.foldl (· ++ ·) Name.anonymous
let id := `_aux ++ (← getMainModule) ++ `_ ++ id
let id := String.intercalate "_" <| id.components.map (·.toString (escape := false))
@ -36,6 +36,6 @@ def elabAuxDef : CommandElab
let (id, _) := DeclNameGenerator.ofPrefix ns |>.mkUniqueName env («infix» := Name.mkSimple id)
let id := id.replacePrefix ns Name.anonymous -- TODO: replace with def _root_.id
elabCommand <|
← `($[$doc?:docComment]? $[$attrs?:attributes]?
← `($[$doc?:docComment]? $[$attrs?:attributes]? $vis:visibility
meta def $(mkIdentFrom (mkNullNode suggestion) id (canonical := true)):ident : $ty := $body)
| _ => throwUnsupportedSyntax

View file

@ -21,18 +21,14 @@ namespace Lean.Elab.Command
$[(name := $name?)]? $[(priority := $prio?)]? $x $args:macroArg* => $rhs) => do
let prio ← liftMacroM do evalOptPrio prio?
let (stxParts, patArgs) := (← args.mapM expandMacroArg).unzip
let name ← match name? with
| some name => pure name.getId
| none => liftMacroM do mkNameFromParserSyntax `binderTerm (mkNullNode stxParts)
let nameTk := name?.getD (mkIdentFrom tk name)
let kind ← elabSyntax (← `($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind syntax%$tk
$[(name := $name?)]? (priority := $(quote prio)) $[$stxParts]* : binderPred))
/- The command `syntax [<kind>] ...` adds the current namespace to the syntax node kind.
So, we must include current namespace when we create a pattern for the following
`macro_rules` commands. -/
let pat : TSyntax `binderPred := ⟨(mkNode ((← getCurrNamespace) ++ name) patArgs).1⟩
let pat : TSyntax `binderPred := ⟨(mkNode kind patArgs).1⟩
elabCommand <|<-
`($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind syntax%$tk
(name := $nameTk) (priority := $(quote prio)) $[$stxParts]* : binderPred
$[$doc?:docComment]? macro_rules%$tk
`($[$doc?:docComment]? macro_rules%$tk
| `(satisfies_binder_pred% $$($x):term $pat:binderPred) => $rhs)
| _ => throwUnsupportedSyntax

View file

@ -50,25 +50,26 @@ def elabElabRulesAux (doc? : Option (TSyntax ``docComment))
pure <| match attrs? with
| some attrs => attrs.getElems.push attr
| none => #[attr]
let vis := Parser.Command.visibility.ofAttrKind attrKind
if let some expId := expty? then
if catName == `term then
`($[$doc?:docComment]? @[$(← mkAttrs `term_elab),*]
`($[$doc?:docComment]? @[$(← mkAttrs `term_elab),*] $vis:visibility
aux_def elabRules $(mkIdent k) : Lean.Elab.Term.TermElab :=
fun stx expectedType? => Lean.Elab.Term.withExpectedType expectedType? fun $expId => match stx with
$alts:matchAlt* | _ => no_error_if_unused% throwUnsupportedSyntax)
else
throwErrorAt expId "syntax category '{catName}' does not support expected type specification"
else if catName == `term then
`($[$doc?:docComment]? @[$(← mkAttrs `term_elab),*]
`($[$doc?:docComment]? @[$(← mkAttrs `term_elab),*] $vis:visibility
aux_def elabRules $(mkIdent k) : Lean.Elab.Term.TermElab :=
fun stx _ => match stx with
$alts:matchAlt* | _ => no_error_if_unused% throwUnsupportedSyntax)
else if catName == `command then
`($[$doc?:docComment]? @[$(← mkAttrs `command_elab),*]
`($[$doc?:docComment]? @[$(← mkAttrs `command_elab),*] $vis:visibility
aux_def elabRules $(mkIdent k) : Lean.Elab.Command.CommandElab :=
fun $alts:matchAlt* | _ => no_error_if_unused% throwUnsupportedSyntax)
else if catName == `tactic || catName == `conv then
`($[$doc?:docComment]? @[$(← mkAttrs `tactic),*]
`($[$doc?:docComment]? @[$(← mkAttrs `tactic),*] $vis:visibility
aux_def elabRules $(mkIdent k) : Lean.Elab.Tactic.Tactic :=
fun $alts:matchAlt* | _ => no_error_if_unused% throwUnsupportedSyntax)
else
@ -92,15 +93,11 @@ def elabElab : CommandElab
$cat $[<= $expectedType?]? => $rhs) => do
let prio ← liftMacroM <| evalOptPrio prio?
let (stxParts, patArgs) := (← args.mapM expandMacroArg).unzip
-- name
let name ← match name? with
| some name => pure name.getId
| none => addMacroScopeIfLocal (← liftMacroM <| mkNameFromParserSyntax cat.getId (mkNullNode stxParts)) attrKind
let nameId := name?.getD (mkIdentFrom tk name (canonical := true))
let pat := ⟨mkNode ((← getCurrNamespace) ++ name) patArgs⟩
elabCommand <|← `(
let kind ← elabSyntax <|← `(
$[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
syntax%$tk$[:$prec?]? (name := $nameId) (priority := $(quote prio):num) $[$stxParts]* : $cat
syntax%$tk$[:$prec?]? $[(name := $name?)]? (priority := $(quote prio):num) $[$stxParts]* : $cat)
let pat := ⟨mkNode kind patArgs⟩
elabCommand <|← `(
$[$doc?:docComment]? elab_rules : $cat $[<= $expectedType?]? | `($pat) => $rhs)
| _ => throwUnsupportedSyntax

View file

@ -23,29 +23,28 @@ open Lean.Parser.Command
withRef (mkNullNode #[tk, rhs]) do
let prio ← liftMacroM <| evalOptPrio prio?
let (stxParts, patArgs) := (← args.mapM expandMacroArg).unzip
-- name
let name ← match name? with
| some name => pure name.getId
| none => addMacroScopeIfLocal (← liftMacroM <| mkNameFromParserSyntax cat.getId (mkNullNode stxParts)) attrKind
/- The command `syntax [<kind>] ...` adds the current namespace to the syntax node kind.
So, we must include current namespace when we create a pattern for the following `macro_rules` commands. -/
let pat := ⟨mkNode ((← getCurrNamespace) ++ name) patArgs⟩
let stxCmd ← `($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
let kind ← elabSyntax (← `($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
syntax$[:$prec?]?
(name := $(name?.getD (mkIdentFrom tk name (canonical := true))))
$[(name := $name?)]?
(priority := $(quote prio):num)
$[$stxParts]* : $cat)
$[$stxParts]* : $cat))
let pat := ⟨mkNode kind patArgs⟩
let rhs := rhs.raw
-- Elide `scoped` for `macro_rules`; this allows for using scoped macros in unscoped macros
-- for back-compat and unlike with `local`, there would be no benefit to enforcing `scoped`.
let mut rulesKind := attrKind
if rulesKind matches `(attrKind| scoped) then
rulesKind ← `(attrKind| )
let macroRulesCmd ← if rhs.getArgs.size == 1 then
-- `rhs` is a `term`
let rhs := ⟨rhs[0]⟩
`($[$doc?:docComment]? macro_rules | `($pat) => Functor.map (@TSyntax.raw $(quote cat.getId.eraseMacroScopes)) $rhs)
`($[$doc?:docComment]? $rulesKind:attrKind macro_rules | `($pat) => Functor.map (@TSyntax.raw $(quote cat.getId.eraseMacroScopes)) $rhs)
else
-- TODO(gabriel): remove after bootstrap
-- `rhs` is of the form `` `( $body ) ``
let rhsBody := ⟨rhs[1]⟩
`($[$doc?:docComment]? macro_rules | `($pat) => `($rhsBody))
elabCommand <| mkNullNode #[stxCmd, macroRulesCmd]
`($[$doc?:docComment]? $rulesKind:attrKind macro_rules | `($pat) => `($rhsBody))
elabCommand macroRulesCmd
| _ => throwUnsupportedSyntax
end Lean.Elab.Command

View file

@ -47,7 +47,8 @@ def elabMacroRulesAux (doc? : Option (TSyntax ``docComment))
let attrs := match attrs? with
| some attrs => attrs.getElems.push attr
| none => #[attr]
`($[$doc?:docComment]? @[$attrs,*]
let vis := Parser.Command.visibility.ofAttrKind attrKind
`($[$doc?:docComment]? @[$attrs,*] $vis:visibility
aux_def macroRules $(mkIdentFrom tk k (canonical := true)) : Macro :=
fun $alts:matchAlt* | _ => no_error_if_unused% throw Lean.Macro.Exception.unsupportedSyntax)
@ -64,9 +65,11 @@ def elabMacroRulesAux (doc? : Option (TSyntax ``docComment))
let attrs := match attrs? with
| some attrs => attrs.getElems.push attr
| none => #[attr]
`($[$doc?:docComment]? @[$attrs,*]
let vis := Parser.Command.visibility.ofAttrKind attrKind
`($[$doc?:docComment]? @[$attrs,*] $vis:visibility
aux_def $(mkIdentFrom tk kind.getId (canonical := true)) $kind : Macro := fun $x:ident => $rhs)
| `($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind macro_rules%$tk (kind := $kind) $alts:matchAlt*) =>
withExporting (isExporting := !attrKind matches `(attrKind| local)) do
withRef (mkNullNode #[tk, mkNullNode alts]) do
elabMacroRulesAux doc? attrs? attrKind tk (← resolveSyntaxKind kind.getId) alts
| _ => throwUnsupportedSyntax

View file

@ -112,54 +112,39 @@ def mkUnexpander (attrKind : TSyntax ``attrKind) (pat qrhs : Term) : OptionT Mac
-- The reference is attached to the syntactic representation of the called function itself, not the entire function application
let lhs ← `($$f:ident)
let lhs := Syntax.mkApp lhs (.mk args)
`(@[$attrKind app_unexpander $(mkIdent c)]
let vis := Parser.Command.visibility.ofAttrKind attrKind
`(@[$attrKind app_unexpander $(mkIdent c)] $vis:visibility
aux_def unexpand $(mkIdent c) : Lean.PrettyPrinter.Unexpander := fun
| `($lhs) => withRef f `($pat)
| _ => throw ())
private def expandNotationAux (ref : Syntax) (currNamespace : Name)
(doc? : Option (TSyntax ``docComment))
(attrs? : Option (TSepArray ``attrInstance ","))
(attrKind : TSyntax ``attrKind)
(prec? : Option Prec) (name? : Option Ident) (prio? : Option Prio)
(items : Array (TSyntax ``notationItem)) (rhs : Term) : MacroM Syntax := do
let prio ← evalOptPrio prio?
-- build parser
let syntaxParts ← items.mapM expandNotationItemIntoSyntaxItem
let cat := mkIdentFrom ref `term
let name ←
match name? with
| some name => pure name.getId
| none => addMacroScopeIfLocal (← mkNameFromParserSyntax `term (mkNullNode syntaxParts)) attrKind
-- build macro rules
let vars := items.filter fun item => item.raw.getKind == ``identPrec
let vars := vars.map fun var => var.raw[0]
let qrhs := ⟨antiquote vars rhs⟩
let attrs? := addInheritDocDefault rhs attrs?
let patArgs ← items.mapM expandNotationItemIntoPattern
/- The command `syntax [<kind>] ...` adds the current namespace to the syntax node kind.
So, we must include current namespace when we create a pattern for the following `macro_rules` commands. -/
let fullName := currNamespace ++ name
let pat : Term := ⟨mkNode fullName patArgs⟩
let stxDecl ← `($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
syntax $[: $prec?]? (name := $(name?.getD (mkIdent name))) (priority := $(quote prio)) $[$syntaxParts]* : $cat)
let macroDecl ← `(macro_rules | `($pat) => ``($qrhs))
let macroDecls ←
@[builtin_command_elab Lean.Parser.Command.notation] def elabNotation : CommandElab
| ref@`($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
notation $[: $prec?]? $[(name := $name?)]? $[(priority := $prio?)]? $items* => $rhs) => do
let attrs? := addInheritDocDefault rhs attrs?
let prio ← liftMacroM <| evalOptPrio prio?
-- build parser
let syntaxParts ← liftMacroM <| items.mapM expandNotationItemIntoSyntaxItem
let cat := mkIdentFrom ref `term
let kind ← elabSyntax (← `($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
syntax $[: $prec?]? $[(name := $name?)]? (priority := $(quote prio)) $[$syntaxParts]* : $cat))
-- build macro rules
let vars := items.filter fun item => item.raw.getKind == ``identPrec
let vars := vars.map fun var => var.raw[0]
let qrhs := ⟨antiquote vars rhs⟩
let patArgs ← liftMacroM <| items.mapM expandNotationItemIntoPattern
let pat : Term := ⟨mkNode kind patArgs⟩
let macroDecl ← `($attrKind:attrKind macro_rules | `($pat) => ``($qrhs))
if isLocalAttrKind attrKind then
-- Make sure the quotation pre-checker takes section variables into account for local notation.
`(section set_option quotPrecheck.allowSectionVars true $macroDecl end)
let opts ← getOptions
let opts := Term.Quotation.quotPrecheck.allowSectionVars.set opts true
withScope (fun sc => { sc with opts }) do
elabCommand macroDecl
else
pure ⟨mkNullNode #[macroDecl]⟩
match (← mkUnexpander attrKind pat qrhs |>.run) with
| some delabDecl => return mkNullNode #[stxDecl, macroDecls, delabDecl]
| none => return mkNullNode #[stxDecl, macroDecls]
@[builtin_macro Lean.Parser.Command.notation] def expandNotation : Macro
| stx@`($[$doc?:docComment]? $[@[$attrs?,*]]? $attrKind:attrKind
notation $[: $prec?]? $[(name := $name?)]? $[(priority := $prio?)]? $items* => $rhs) => do
-- trigger scoped checks early and only once
let _ ← toAttributeKind attrKind
expandNotationAux stx (← Macro.getCurrNamespace) doc? attrs? attrKind prec? name? prio? items rhs
| _ => Macro.throwUnsupported
elabCommand macroDecl
if let some delabDecl := (← liftMacroM <| mkUnexpander attrKind pat qrhs |>.run) then
elabCommand delabDecl
| _ => throwUnsupportedSyntax
end Lean.Elab.Command

View file

@ -314,7 +314,7 @@ private def declareSyntaxCatQuotParser (catName : Name) : CommandElabM Unit := d
```
It generates the names `term_+_` and `term[_,]`
-/
partial def mkNameFromParserSyntax (catName : Name) (stx : Syntax) : MacroM Name := do
private partial def mkNameFromParserSyntax (catName : Name) (stx : Syntax) : MacroM Name := do
mkUnusedBaseName <| Name.mkSimple <| appendCatName <| visit stx ""
where
visit (stx : Syntax) (acc : String) : String :=
@ -368,13 +368,14 @@ def isLocalAttrKind (attrKind : Syntax) : Bool :=
/--
Add macro scope to `name` if it does not already have them, and `attrKind` is `local`.
-/
def addMacroScopeIfLocal [MonadQuotation m] [Monad m] (name : Name) (attrKind : Syntax) : m Name := do
private def addMacroScopeIfLocal [MonadQuotation m] [Monad m] (name : Name) (attrKind : Syntax) : m Name := do
if isLocalAttrKind attrKind && !name.hasMacroScopes then
MonadQuotation.addMacroScope name
else
return name
@[builtin_command_elab «syntax»] def elabSyntax : CommandElab := fun stx => do
/-- Elaborates the given `syntax` command and returns the new node kind. -/
def elabSyntax (stx : Syntax) : CommandElabM Name := do
let `($[$doc?:docComment]? $[ @[ $attrInstances:attrInstance,* ] ]? $attrKind:attrKind
syntax%$tk $[: $prec? ]? $[(name := $name?)]? $[(priority := $prio?)]? $[$ps:stx]* : $catStx) := stx
| throwUnsupportedSyntax
@ -390,24 +391,42 @@ def addMacroScopeIfLocal [MonadQuotation m] [Monad m] (name : Name) (attrKind :
| none => pure precDefault
let name ← match name? with
| some name => pure name.getId
| none => addMacroScopeIfLocal (← liftMacroM <| mkNameFromParserSyntax cat syntaxParser) attrKind
| none =>
let base ← liftMacroM <| mkNameFromParserSyntax cat syntaxParser
let mut name := base
let mut i := 1
-- Avoid name conflicts, for which we have to check both public and private name
while (←
let fullName := (← getCurrNamespace) ++ name
hasConst fullName <||>
(withoutExporting <| hasConst (mkPrivateName (← getEnv) fullName))) do
name := base.appendIndexAfter i
i := i + 1
pure name
let prio ← liftMacroM <| evalOptPrio prio?
let idRef := (name?.map (·.raw)).getD tk
let stxNodeKind := (← getCurrNamespace) ++ name
let mut stxNodeKind := (← getCurrNamespace) ++ name
if attrKind matches `(attrKind| local) then
stxNodeKind := mkPrivateName (← getEnv) stxNodeKind
let catParserId := mkIdentFrom idRef (cat.appendAfter "_parser")
let (val, lhsPrec?) ← runTermElabM fun _ => Term.toParserDescr syntaxParser cat
let declName := name?.getD (mkIdentFrom idRef name (canonical := true))
let attrInstance ← `(attrInstance| $attrKind:attrKind $catParserId:ident $(quote prio):num)
let attrInstances := attrInstances.getD { elemsAndSeps := #[] }
let attrInstances := attrInstances.push attrInstance
let vis := Parser.Command.visibility.ofAttrKind attrKind
let d ← if let some lhsPrec := lhsPrec? then
`($[$doc?:docComment]? @[$attrInstances,*] meta def $declName:ident : Lean.TrailingParserDescr :=
`($[$doc?:docComment]? @[$attrInstances,*] $vis:visibility meta def $declName:ident : Lean.TrailingParserDescr :=
ParserDescr.trailingNode $(quote stxNodeKind) $(quote prec) $(quote lhsPrec) $val)
else
`($[$doc?:docComment]? @[$attrInstances,*] meta def $declName:ident : Lean.ParserDescr :=
`($[$doc?:docComment]? @[$attrInstances,*] $vis:visibility meta def $declName:ident : Lean.ParserDescr :=
ParserDescr.node $(quote stxNodeKind) $(quote prec) $val)
trace `Elab fun _ => d
withMacroExpansion stx d <| elabCommand d
return stxNodeKind
@[builtin_command_elab «syntax»] private def elabSyntaxAux : CommandElab := fun stx =>
discard <| elabSyntax stx
@[builtin_command_elab «syntaxAbbrev»] def elabSyntaxAbbrev : CommandElab := fun stx => do
let `($[$doc?:docComment]? syntax $declName:ident := $[$ps:stx]*) ← pure stx | throwUnsupportedSyntax

View file

@ -79,8 +79,13 @@ def addMacroStack {m} [Monad m] [MonadOptions m] (msgData : MessageData) (macroS
msgData
def checkSyntaxNodeKind [Monad m] [MonadEnv m] [MonadError m] (k : Name) : m Name := do
if Parser.isValidSyntaxNodeKind (← getEnv) k then pure k
else throwError "failed"
if Parser.isValidSyntaxNodeKind (← getEnv) k then
return k
if !(← getEnv).isExporting && !isPrivateName k then
let k := mkPrivateName (← getEnv) k
if Parser.isValidSyntaxNodeKind (← getEnv) k then
return k
throwError "failed"
def checkSyntaxNodeKindAtNamespaces [Monad m] [MonadEnv m] [MonadError m] (k : Name) : Name → m Name
| n@(.str p _) => checkSyntaxNodeKind (n ++ k) <|> checkSyntaxNodeKindAtNamespaces k p

View file

@ -885,6 +885,12 @@ used to generate visibility syntax for declarations that is independent of the p
def visibility.ofBool (isPublic : Bool) : TSyntax ``visibility :=
Unhygienic.run <| if isPublic then `(visibility| public) else `(visibility| private)
/--
Returns syntax for `private` if `attrKind` is `local` and `public` otherwise.
-/
def visibility.ofAttrKind (attrKind : TSyntax ``Term.attrKind) : TSyntax ``visibility :=
visibility.ofBool <| !attrKind matches `(attrKind| local)
end Command
namespace Term

View file

@ -0,0 +1,5 @@
/-! `notation` should avoid name conflicts, also between local/global. -/
namespace Foo
local notation "foo" => 1
notation "foo" => 1

View file

@ -1,4 +1,6 @@
scopedInstanceOutsideNamespace.lean:1:0-2:38: error: Scoped attributes must be used inside namespaces
"true"
scopedInstanceOutsideNamespace.lean:6:0-6:30: error: Scoped attributes must be used inside namespaces
scopedInstanceOutsideNamespace.lean:6:0-6:30: error: invalid syntax node kind '«term_+__1»'
scopedInstanceOutsideNamespace.lean:6:0-6:30: error: Scoped attributes must be used inside namespaces
scopedInstanceOutsideNamespace.lean:8:2-8:17: error: Scoped attributes must be used inside namespaces

View file

@ -1,6 +1,6 @@
syntaxPrec.lean:1:17-1:21: error: unexpected token '<|>'; expected ':'
[Elab.command] @[term_parser 1000]
meta def «termFoo*_» : Lean.ParserDescr✝ :=
public meta def «termFoo*_» : Lean.ParserDescr✝ :=
ParserDescr.node✝ `«termFoo*_» 1022
(ParserDescr.binary✝ `andthen (ParserDescr.symbol✝ "foo")
(ParserDescr.binary✝ `orelse (ParserDescr.nodeWithAntiquot✝ "*" `token.«*» (ParserDescr.symbol✝ "*"))

View file

@ -8,8 +8,11 @@ local infix:50 " ≺ " => LE.le
#check 1 ≺ 2
local macro "my_refl" : tactic =>
`(tactic| rfl)
-- It is possible to bind a local macro to a public auto param but we must opt into it explicitly by
-- separating the `macro_rules` and removing the `local` from it.
local syntax "my_refl" : tactic
macro_rules
| `(tactic| my_refl) => `(tactic| rfl)
def f (x y : Nat) (_h : x = y := by my_refl) := x

View file

@ -0,0 +1 @@
lean4