A Mini Macro Processor

I've been fascinated by the GNU M4 macro processor for a long time. It's a titan from a previous age when we didn't already have a tool for everything. If I didn't have purpose-built tools like markdown and Pandoc, I might need something like M4. On the belt of Unix power tools, M4 is a backhoe.

Recently, I came across a 2007 Dr. Dobbs article by Jon Bentley, “m1: A Mini Macro Processor”. In it, he describes the use-case for a simple macro processor and why such a thing may be an interesting object of study. I saw in it a way to reap at least some of the benefits of a macro processor without the risk of including M4 in my tool chain. I had a moment of downtime, so I copied the code and inserted m1 into my publishing tool chain.

I author using markdown in Vim. When I want to publish, the content is sent to Pandoc for conversion to HTML (sometimes to docx). Pandoc also pulls in Foundation CSS for formatting and layout control. This gives me great-looking, self-contained HTML documents that I can share over email. I added m1 as the first step in the publishing process. Content now goes to m1 for macro expansion before being sent to Pandoc. I also added mappings in Vim that let me preview expanded text.

Now that I've set this up, I'm still not convinced that I need it. The main benefit so far has been a macro that expands to a portion of the Azure DevOps item detail URL. If I have a work item number, I can write @link@12345 and it will be expanded to “http:/something.azure.com/Even%20More%Stuff/items/edit/12345” That URL is pretty long and the benefit is that I no longer have to alt-tab away to go copy it to my clipboard.

The other somewhat practical use is a macro that formats tables using Foundation CSS classes. By default, a table in Foundation will be full-width, but I can shrink it down using the XY Grid. I just wrap my table in a couple of divs with the right classes. The “:::"s are how you specify arbitrary divs in Pandoc's markdown.

:::{.grid-x}
::::{.cell .shrink}
Book | Author
----|----
Programming Pearls | Jon Bentley
Thinking Forth | Leo Brodie
Gödel, Escher, Bach | Douglas Hofstadter
::::
:::

This is not inconvenient to type. I used to type it out every time I needed it, but I decided it was a worthy use-case for m1. My macro started out like this:

@define TB :::{.grid-x}\
  ::::{.cell .shrink}
@define TE ::::\
  :::

Here, I've used troff-style mnemonics for Table Begin and Table End. It's very straight-forward, but I could make it a little more powerful. Sometimes, I might want to let the table breathe a bit more. I extended the macro definition to use an optional width value. This is where we get into nested macros, something mentioned in passing in Bentley's article.

@longdefine TB
@define _width .shrink
@if width
@define _width .small-@width@
@fi
:::{.grid-x}
::::{.cell @_width@} 
@longend

If width is set to zero or undefined, the grid cell will be set to fit its contents, constraining the table. Otherwise, the width of the grid cell that contains the table will be set to my given width. To create a table that takes up half of the available width, I can set the width to 6.

@define width 6
@TB@
Book | Author
----|----
Programming Pearls | Jon Bentley
Thinking Forth | Leo Brodie
Gödel, Escher, Bach | Douglas Hofstadter
@TE@

I often advise people to solve only those problems that they actually have. With m1, it's easy to imagine solutions to problems that I don't have. For example, it's almost never necessary for me to embed calculations, but I created a @calc directive, anyway. I incorporated functionality from my dc-inspired calculator, conveniently also written in Awk. I can imagine possibly using this at some point, but it's not something I need right now.

@define radius 12
@calc area 3.14159 @radius@ d * *
The area of a circle with radius @radius@ is @area@.

Recently, I've been apartment hunting. Many places are offering a month or two of free rent to drive business. The discount is a nice incentive, but I need to be sure I can still afford the place at full price.

@define rent 1295
@define parking 175
@define term 12
@define mofreerent 2
@define mofreeparking 2
@calc year2 @rent@ @parking@ + @term@ *
@calc year1 @year2@ @rent@ @mofreerent@ * - @mofreeparking@ @parking@ * -
You will pay $@year1@ in the first year and $@year2@ each year thereafter.

I could also create a macro definition that represents a series of operations. This is starting to look very Forth-y. Is there a Forth equivalent of Greenspun's tenth rule? Here, I've defined a macro, timesminus, that multiplies the top two numbers on the stack and subtracts their product from the next number on the stack.

@define rent 1295
@define parking 175
@define term 12
@define mofreerent 2
@define mofreeparking 2
@define timesminus * -
@calc year2 @rent@ @parking@ + @term@ *
@calc year1 @year2@ @rent@ @mofreerent@ @timesminus@ @mofreeparking@ @parking@ @timesminus@
You will pay $@year1@ in the first year and $@year2@ each year thereafter.

Beyond that, I get into weird tricks and experiments, like playing with indirection. Of indirection, Wikipedia says,

The most common form of indirection is the act of manipulating a value through its memory address.

In the context of m1, the macro name acts as the memory address. We can define a macro using the name of another macro.

@define book Programming Pearls
@define author Jon Bentley
@define description @book@
@description@

Processing this results only in “Programming Pearls”. To get there, m1 translates @description@ to @book@, and then @book@ to Programming Pearls. We could as easily define description as “@book@ by @author@”. This allows us to compose macros using other macros. We could also use this behavior to compose macro names. We can use this to build faux matrices using a sort of associative array.

@define matrix00 A
@define matrix01 B
@define matrix10 C
@define matrix11 D
@define ref @matrix@a@@b@@
@define a 1
@define b 0
@ref@

Processing this results in “C”. This could be used to represent a simple decision table. For example, what we will do on Saturday depends on the weather and the condition of our bicycle.

@define matrix00 Stay in an play games
@define matrix01 Short ride with rain gear
@define matrix10 Walk to get ice cream
@define matrix11 Ride to the beach
@define activity @matrix@sunny@@bikeOK@@
@define sunny 1
@define bikeOK 0
@activity@

Since it's sunny out, but our bike is not OK, we will walk to get ice cream. The macro name @activity@ is replaced with its definition, @matrix@sunny@@bikeOK@@. This resolves to @matrix10@ which is, in turn, translated to “Walk to get ice cream”. We're cheating a bit, though – if it happens that @matrix@ is defined as a macro, we'll spend our Saturday debugging, instead.

You might be wondering if we can put m1 into an infinite loop by defining a circular reference. Yes, you can. Don't try this at home.

@comment infinite loop!
@define a @b@
@define b @a@
@a@

There are many more possibilities, but I'll leave it there. I hope I've impressed upon you the power and possibility that a macro processor represents. Read up on GNU M4, or check out my version of Bentley's m1.

Happy hacking!