fix: make sure syntax nodes always run their formatters (#4631)

Now syntax nodes have their formatters run even if the parsers they wrap
are all arity zero. This fixes an issue where if `ppSpace` appears in a
`macro`/`elab` then it does not format with a space due to the fact that
macro argument processing wraps this as `group(ppSpace)`, and `ppSpace`
has arity zero.

Implementation note: the fix is to make the `visitArgs` formatter
combinator always visit the last child, even if it does not exist (in
which case the visited node will be `Syntax.missing`). To compensate,
parser combinators like many and optional need to be sure to keep track
of whether there any children. Only optional's needed to be modified.

Closes #4561
This commit is contained in:
Kyle Miller 2024-07-03 00:45:34 -07:00 committed by GitHub
parent 7ef95cd30b
commit c2edae92c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 5 deletions

View file

@ -129,11 +129,13 @@ def pushLine : FormatterM Unit :=
def pushAlign (force : Bool) : FormatterM Unit :=
pushWhitespace (.align force)
/-- Execute `x` at the right-most child of the current node, if any, then advance to the left. -/
/--
Execute `x` at the right-most child of the current node, if any, then advance to the left.
Runs `x` even if there are no children, in which case the current syntax node will be `.missing`.
-/
def visitArgs (x : FormatterM Unit) : FormatterM Unit := do
let stx ← getCur
if stx.getArgs.size > 0 then
goDown (stx.getArgs.size - 1) *> x <* goUp
goDown (stx.getArgs.size - 1) *> x <* goUp
goLeft
/-- Execute `x`, pass array of generated Format objects to `fn`, and push result. -/
@ -454,7 +456,9 @@ def manyNoAntiquot.formatter (p : Formatter) : Formatter := do
@[combinator_formatter many1NoAntiquot] def many1NoAntiquot.formatter (p : Formatter) : Formatter := manyNoAntiquot.formatter p
@[combinator_formatter optionalNoAntiquot]
def optionalNoAntiquot.formatter (p : Formatter) : Formatter := visitArgs p
def optionalNoAntiquot.formatter (p : Formatter) : Formatter := do
let stx ← getCur
visitArgs <| unless stx.getArgs.isEmpty do p
@[combinator_formatter many1Unbox]
def many1Unbox.formatter (p : Formatter) : Formatter := do
@ -467,7 +471,7 @@ def many1Unbox.formatter (p : Formatter) : Formatter := do
@[combinator_formatter sepByNoAntiquot]
def sepByNoAntiquot.formatter (p pSep : Formatter) : Formatter := do
let stx ← getCur
visitArgs <| (List.range stx.getArgs.size).reverse.forM fun i => if i % 2 == 0 then p else pSep
visitArgs <| stx.getArgs.size.forRevM fun i => if i % 2 == 0 then p else pSep
@[combinator_formatter sepBy1NoAntiquot] def sepBy1NoAntiquot.formatter := sepByNoAntiquot.formatter

50
tests/lean/run/4561.lean Normal file
View file

@ -0,0 +1,50 @@
import Lean.Elab.Command
/-!
# 4561: group(ppSpace) was not pretty printing with a space
-/
/-!
Example from the issue. Here, ppSpace is wrapped in a group due to macro argument handling.
-/
macro (name := useSyntax) "use0" ppSpace arg:term : tactic => `(tactic| sorry)
/-- info: use0 .succ✝ 0 -/
#guard_msgs in
run_cmd do
Lean.logInfo <| ← `(tactic| use0 .succ 0)
/-!
Version of this using `syntax` to test it without macro argument handling.
-/
syntax "use1" ppSpace term,+ : tactic
syntax "use2" group(ppSpace) term,+ : tactic
/--
info: use1 .succ✝ 0
---
info: use2 .succ✝ 0
-/
#guard_msgs in
run_cmd Lean.Elab.Command.liftTermElabM do
Lean.logInfo <| ← `(tactic| use1 .succ 0)
Lean.logInfo <| ← `(tactic| use2 .succ 0)
/-!
Check that fix even works inside other nodes.
-/
syntax myPPSpace := ppSpace
syntax "use3" myPPSpace term,+ : tactic
syntax "use4" group(myPPSpace) term,+ : tactic
/--
info: use3 .succ✝ 0
---
info: use4 .succ✝ 0
-/
#guard_msgs in
run_cmd Lean.Elab.Command.liftTermElabM do
Lean.logInfo <| ← `(tactic| use3 .succ 0)
Lean.logInfo <| ← `(tactic| use4 .succ 0)