Zulip Chat Archive

Stream: lean4

Topic: Pass optional syntax in macro


Patrick Massot (Jul 15 2023 at 15:18):

In the following code, a call to tactic bar is meant to expand to the same call to foo. But I don't know how to pass the optional syntax. Both variations below fail.

open Lean Parser Tactic

macro "foo" (location)? : tactic => `(tactic|done)

macro "bar" loc:(location)? : tactic => `(tactic|foo $loc)

macro "bar" loc:(location)? : tactic => `(tactic|foo $(mkOptionalNode loc))

Mac Malone (Jul 15 2023 at 15:32):

@Patrick Massot $[...]? is used for optional syntax:

macro "bar" loc?:(location)? : tactic => `(tactic|foo $[$loc?]?)

Patrick Massot (Jul 15 2023 at 15:37):

Thanks a lot. It almost work in my real use case. It fails in the presence of a mysterious & in the original syntax;

Patrick Massot (Jul 15 2023 at 15:38):

Something like

macro "foo" (&"baz")? (location)? : tactic => `(tactic|done)

macro "bar" ba:(&"baz")? loc:(location)? : tactic => `(tactic|foo $[$ba]? $[$loc]?)

Sebastian Ullrich (Jul 15 2023 at 15:53):

The syntax for individual tokens is a bit different

macro "bar" ba:(&"baz")? loc:(location)? : tactic => `(tactic|foo $[baz%$ba]? $[$loc]?)

Patrick Massot (Jul 15 2023 at 16:00):

Thanks!

Patrick Massot (Jul 15 2023 at 16:21):

Next I need to remember how to write a macro which expand to several tactic calls, fighting the difference between tactic and tactic_seq.

Patrick Massot (Jul 15 2023 at 16:22):

Say I want to add done after foo $[baz%$ba]? $[$loc]?, how do I do that?

Sebastian Ullrich (Jul 15 2023 at 16:24):

You can use parentheses to turn a tactic sequence into a single tactic

Patrick Massot (Jul 15 2023 at 16:25):

I knew there was a simple trick but I couldn't remember it. Thanks!

Patrick Massot (Nov 03 2023 at 00:45):

I hit again the same issue as in July but I can't get the above answer to work. If have a tactic whose definition starts with

elab tok:"rw_ineq" rules:rwRuleSeq loc:(location)? withArg:((" with " (colGt binderIdent)+)?) : tactic =>

and I want a macro rwa_ineq with the exact same syntax that call rw_ineq and then assumption. I tried

macro "rwa_ineq " rws:rwRuleSeq loc:(location)? withArg:(" with " (colGt binderIdent)+)? : tactic =>
  `(tactic| (rw_ineq $rws:rwRuleSeq $[$loc:location]? $[$withArg:location]?; assumption))

but that doesn't work.

Full code is

import Lean

open Lean Parser Meta Elab Term Tactic

elab tok:"rw_ineq" rules:rwRuleSeq loc:(location)? withArg:((" with " (colGt Lean.binderIdent)+)?) : tactic =>
  pure ()

macro "rwa_ineq " rws:rwRuleSeq loc:(location)? withArg:(" with " (colGt Lean.binderIdent)+)? : tactic =>
  `(tactic| (rw_ineq $rws:rwRuleSeq $[$loc:location]? $[$withArg:location]?; assumption))

Kyle Miller (Nov 03 2023 at 00:56):

Here's one way:

import Lean

open Lean Parser Meta Elab Term Tactic

syntax withClause := " with " (colGt Lean.binderIdent)?

elab tok:"rw_ineq" rules:rwRuleSeq loc:(location)? withArg:(withClause)? : tactic =>
  pure ()

macro "rwa_ineq " rws:rwRuleSeq loc:(location)? withArg:(withClause)? : tactic =>
  `(tactic| (rw_ineq $rws:rwRuleSeq $[$loc:location]? $[$withArg]?; assumption))

Kyle Miller (Nov 03 2023 at 00:58):

Here's another:

import Lean

open Lean Parser Meta Elab Term Tactic

elab tok:"rw_ineq" rules:rwRuleSeq loc:(location)? withArg:((" with " (colGt Lean.binderIdent)+)?) : tactic =>
  pure ()

syntax "rwa_ineq " rwRuleSeq (location)? (" with " (colGt Lean.binderIdent)+)? : tactic

macro_rules
  | `(tactic| rwa_ineq $rws $[$loc]? $[with $args*]?) =>
    `(tactic| rw_ineq $rws $[$loc]? $[with $args*]?)

Patrick Massot (Nov 03 2023 at 01:00):

Great, the first solution works (with minor ajustements to the code)! Thanks!

Patrick Massot (Nov 03 2023 at 01:01):

I'm still curious to understand why my original code didn't work (and it would be really great to have any of this documented somewhere).

Alex Keizer (Jun 03 2024 at 13:12):

I'm running into similar issues: I currently have a macro like this

macro res:mlir_op_operand " = " "llvm.mlir.constant" x:num " : " t:mlir_type : mlir_op =>

I'd like to make the " : " t:mlir_type part optional. In a macro_rules, I can write $[: $t]? and everything works, but in a macro this doesn't seem to work. Going by the messages in this thread, the syntax for macro seems to be t:(" : " mlir_type), but now the bound t is of type TSyntax Name.anonymous, rather than the t : TSyntax `mlir_type I originally had.

I guess I could index into it, but I'd rather not. Is there not an equivalent of $[: $t]? which allows me to bind just the part I actually want?

Kyle Miller (Jun 03 2024 at 13:31):

Not to my knowledge. I would suggest either the withClause example or writing a separate syntax and using macro_rules instead.

Alex Keizer (Jun 03 2024 at 13:37):

The annoying thing is that the " : " token occurs only in the input, in the thing I'm rewriting to I only want the actual t. I was hoping to avoid the duplication macro_rules gives, but knowing there's no good alternatiove will let me stop worrying about it, thanks!

Alex Keizer (Jun 04 2024 at 15:13):

I've got another question about optional syntax and macros, now about how to deal with optional parts that are just atomic tokens. How do I combine the two match arms into one in the following MWE:

import Lean
open Lean

syntax neg_num := "-"? num

/-- Convert a possibly negated numeral into a term representing the same value -/
def negNumToTerm : TSyntax ``neg_num  MacroM Term
-- This works, but I'd like to combine the first two cases
  | `(neg_num| $x:num) => `($x:num)
  | `(neg_num| -$x:num) => `(-$x:num)
  | _ => Macro.throwUnsupported


def negNumToTerm₂ : TSyntax ``neg_num  MacroM Term
  | `(neg_num| $[-]? $x:num) =>
  --           ^^^^^ I can match this part optionally
    `(term| $[-]? $x:num)
  --        ^^^^^ This, however, Lean does not like (`unexpected token ...`)
  -- I'm unsure how to then output the minus conditionally, or even how to
  -- give a name to the matched minus to check whether it was present or not in
  -- the macro body
  | _ => Macro.throwUnsupported

Damiano Testa (Jun 04 2024 at 15:34):

You can give a name to the token, by using the syntax $[-%$minus]?.

I'm not sure how to feed minus back into the syntax, though.

Damiano Testa (Jun 04 2024 at 15:35):

Actually, this seems to work, but I am not sure if it is what you want:

  | `(neg_num| $[-%$minus]? $x:num) => `($(minus.getD default) $x:num)

Alex Keizer (Jun 04 2024 at 15:52):

Thanks! I didn't know about the % syntax. It'd be cool if we could somehow control which token controls a $[...]? antiquotation (i.e., on the RHS), without that token being part of the actual output.
For now, I'll just accept the two-match arms version is probably the most concise it gets, without impacting readability too much

Kyle Miller (Jun 04 2024 at 17:46):

The - isn't just a token here; in -$x:num it's parsing as the syntax for negation. It looks something like this:

  • Syntax of kind «term-_»
    • arg[0]: - atom
    • arg[1]: ... syntax for $x ...

You can't conditionally include - or not because this «term-_» node is an essential part of it being negation.

Kyle Miller (Jun 04 2024 at 17:48):

If you want to combine the cases, you could at least do this:

def negNumToTerm : TSyntax ``neg_num  MacroM Term
  | `(neg_num| $[-%$min]? $x:num) =>
    if min.isSome then
      `(-$x)
    else
      `($x)
  | _ => Macro.throwUnsupported

Kyle Miller (Jun 04 2024 at 17:53):

Damiano Testa said:

  | `(neg_num| $[-%$minus]? $x:num) => `($(minus.getD default) $x:num)

This is surely not correct. If you're ever writing $(⟨x⟩) you have to proceed very carefully because it's converting x to have type Syntax and then allowing it to be used as TSyntax k for any k, even if it's not correct.

If you look at what this code expands to (using #print negNumToTerm) you can see that it takes minus, uses default if it's missing (which for syntax is Syntax.missing, which will signal an error during elaboration), and then creates a function application Syntax with this result as the function and $x as the argument.

Damiano Testa (Jun 04 2024 at 18:15):

Oh, right: juxtaposition is function application!

Robert Maxton (Feb 15 2025 at 22:04):

Same question:

import Batteries.Tactic.Init

namespace Tactic.Conv
macro (name := cast) "cast " h?:(term)? : conv => `(conv | equals (cast ?h ?rhs) => rfl $[; exact $h?]?)

Robert Maxton (Feb 15 2025 at 22:05):

I can get it to compile as

macro (name := cast) "cast " h?:(term)? : conv => do
  let tacs  if let some h := h? then `(tacticSeq| rfl; exact $h ) else `(tacticSeq| rfl)
  `(conv | equals (cast ?h ?rhs) => $tacs)

(this doesn't really do the thing I want, but that's a separate problem)
but I'd like to understand why the first version doesn't work.

Kyle Miller (Feb 15 2025 at 22:18):

Here's a trick:

macro (name := cast) "cast " h?:(term)? : conv =>
  `(conv | equals (cast ?h ?rhs) => rfl; $[exact $(h?.toArray)]*)

Kyle Miller (Feb 15 2025 at 22:19):

The issue is that $[...]? is for optional syntax nodes, but a tactic sequence is a sequence of syntax nodes. We're able to splice in an array of nodes this way, and we can use Option.toArray to make it be compatible.

Kyle Miller (Feb 15 2025 at 22:20):

I wonder if $[...]* should be friendly able to handle Option like this automatically?

Or maybe sequences should accept $[...]? syntax too in antiquotations?

(Or both?)

Robert Maxton (Feb 15 2025 at 22:25):

Ahhh, I see. I vaguely recall running into a similar problem last year, too, where the "can't simultaneously expand multiple sequences of different lengths" restriction turned out to mean that you couldn't expand an optional term and an array of length > 1.

Robert Maxton (Feb 15 2025 at 22:26):

Anyway, thanks!


Last updated: May 02 2025 at 03:31 UTC