macro_rules Tutorial

macro_rules allow us to define macros in terms of pairs of input and output patterns, referred to as matchers and transcribers. Each pair makes up a rule, and a group of rules is, well, a macro_rules.

Our First macro_rules

Let’s dive right in! Create a new script named first_macro_rules.py and add the following:

# coding: macro-polo

macro_rules! my_macro:
    []:
        'My first macro!'

print(my_macro!())
print('My first macro!')
$ python3 first_macro_rules.py 
My first macro!

We defined a macro with a single rule, matching an empty token sequence. What if we try invoking it on a non-empty input?

# coding: macro-polo

macro_rules! my_macro:
    []:
        'My first macro!'

print(my_macro!('hello'))
$ python3 first_macro_rules.py 
Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/codec.py", line 25, in _decode
    result = stringify(macro(tokens) or tokens)
                       ^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/macros/super.py", line 26, in __call__
    if (new_tokens := macro(tokens)) is not None:
                      ^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/macros/super.py", line 51, in __call__
    if (new_tokens := macro(tokens)) is not None:
                      ^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/macros/super.py", line 102, in __call__
    if (transformed_inner := self(inner_tokens)) is not None:
                             ^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/macros/super.py", line 83, in __call__
    new_tokens, match_size = macro(tokens)
                             ^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/macros/function.py", line 98, in __call__
    result = self._invoke_macro(name, body)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/macro-polo/envs/stable/lib/python3.12/site-packages/macro_polo/macros/function.py", line 84, in _invoke_macro
    raise MacroError(
macro_polo.MacroError: invoking function-like macro 'my_macro': body didn't match expected pattern
SyntaxError: encoding problem: macro-polo

We got a macro_polo.MacroError:

macro_polo.MacroError: invoking function-like macro 'my_macro': body didn't match expected pattern

Let’s add a new rule to handle this case:

# coding: macro-polo

macro_rules! my_macro:
    []:
        'My first macro!'

    ['hello']:
        'Got "hello"'

print(my_macro!('hello'))
print('Got "hello"')
$ python3 first_macro_rules.py 
Got "hello"

Great! But this only handles the specific token 'hello'. How can we handle any token?

Capture Variables

Capture variables allow us to capture tokens of a specific type, binding them to a name that we can later use in the transcriber. Capture variables have the syntax $name:type in matchers, and just $name in transcribers.

Let’s modify our rule to accept any string token:

# coding: macro-polo

macro_rules! my_macro:
    []:
        'My first macro!'

    [$s:string]:
        f'Got {$s!r}'

print(my_macro!('howdy'))
print(f'Got {'howdy'!r}')
$ python3 first_macro_rules.py 
Got 'howdy'

Nice! But what if we want to accept any number of strings?

Repeaters

That brings us to repeaters. Repeaters let us—wait for it—repeat patterns. They come in three flavors, or repition modes:

  • $(pattern)? matches pattern ≤1 times

  • $(pattern)* matches pattern ≥0 times

  • $(pattern)+ matches pattern ≥1 times

Additionally, the latter two accept an optional separator token between the closing parenthesis and mode indicator.

Let’s see an example:

# coding: macro-polo

macro_rules! my_macro:
    []:
        'My first macro!'

    [$($s:string);+]:
        f'Got {[ $(repr($s)),* ]}'

print(my_macro!('hey'; 'hi'; "what's up"))
print(f'Got {[repr('hey'), repr('hi'), repr("what's up")]}')
$ python3 first_macro_rules.py 
Got ["'hey'", "'hi'", '"what\'s up"']