Macten Devlog 1
30 Dec 2023, Ochawin A.Macten
is short for macro extensions. As the name suggests, Macten
focuses on macros. The project’s goal is to offer a build step involving macros for any given text-based programming language.
1. Background
My first exposure to macros came from using the #define
directive in C
. To be honest, initially, I found it unclear why such a feature would be useful. However, as time passed and I gained more experience, I began to love it more and more. It quickly became one of my favorite features, despite its very apparent flaws and limitations.
I was frustrated to find out that other languages like Java
and Python
lacked any macro features (though, of course, they have constructs that replicate macro behavior). I am fully aware that a language like C
needs macros, but the same cannot be said for other languages, especially dynamically typed ones. I would never advocate the usage of macros over well-defined constructs that achieve the same behavior (though I will showcase them for demonstrative purposes).
More than anything, Macten
serves as a learning project for me. Regardless of whether it ends up being necessary or not, I want to try to implement it and see it for myself. Also, I genuinely believe that a fixation on doing only necessary things will do nothing but harm your learning potential; it serves as a terrible stance for learning.
2. Primer
In case you are not familiar with macros, let’s have a little primer on what macros are and what they look like in the wild.
2.1 C
Let’s start with the most basic example of a macro system in a langauge, of course it’s from our beloved C
.
2.1.1 Parameterless
As the name implies, a parameterless macro is a macro which takes in no input. This is also the most basic type of macro since it is simply just string substitution.
In C
we can define a parameterless macro using the #define
pre-preprocessor directives.
// Defining constants.
#define PI 3.14159f
float calculate_circle_area(float radius)
{
// Expands into: return radius * radius * 3.14159;
return radius * radius * PI;
}
2.1.2 Macro with args
A macro can also be fed arguments as if it were a function. One thing to take note of is that the call site of the macro may look exactly identical to the call site of a function (atleast in the case of C
’s #define
) and this may lead to a bunch of maintenance issues. To mitigate this problem, a convention is often adopted.
Here is an example of how a macro which takes in parameters can be declared:
// `##` is a special operator used by the pre-processor to concatenate tokens together.
#define JOIN(a,b) a ## b
#define PI 3.14159f
#define CALCULATE_CIRCLE_AREA_FROM_RADIUS(radius) (radius * radius * PI)
void foo()
{
// Expands into: float var1 = (0.5f * 0.5f * PI);
float JOIN(var, 1) = CALCULATE_CIRCLE_AREA_FROM_RADIUS(0.5f);
}
JOIN
does for instance.2.1.3 Multi-line macro
While it might not exactly be a type of macro by itself, I think it’s important to mention anyways. Macros are not restricted to only be one-liners.
In C, you might have seen something like this:
#define foo(...) \
do { \
// stmt 1... \
// stmt 2... \
} while (0)
do {...} while (0)
is a well known trick used for bundling statements together. By using the do-while trick, we can invoke the multi-line macro and terminate it with a ;
as we would with any other function.Why not just exclude
;
from the last statement?
Though omitting ;
at the end of the last statement is an option, it poses maintenance challenges. Adding another statement later would require careful consideration, adding to potential points of maintenance. Furthermore, this approach introduces the problem of macro hygiene
which will be covered shortly. Also, by encapsulating the macro body within a do-while
block, when expanding the macro, the statements belonging to the macro block can be cleary seen. However the same can not be said with just omitting ;
.
Notice that at the end of each line the backslash character \
is needed. By using the backslash character before the newline, the newline is skipped, allowing the macro to continue consuming the next line. This is required due to how macros are parsed, it seems to obey the following syntax: <#define> <*> <newline>
. While it might not seem like a big deal, this is actually a very big flaw. If there is any other character after the newline, perhaps a space, your macro would be broken, without you being able to tell why.
2.2 Rust
Moving onto something more modern. Rust
offers a macro system in the form of macro_rules!
. Here are the previous examples rewritten in rust
.
// Parameterless.
macro_rules! PI {
() => {
3.14159
}
}
// With arguments.
macro_rules! CALCULATE_CIRCLE_AREA_FROM_RADIUS {
($radius: expr) => {
($radius * $radius * PI![])
}
}
// Usage.
fn main () {
// Notice that `println` is also a macro.
// We can either use `()`/`[]` for macro args.
println!("{}", CALCULATE_CIRCLE_AREA_FROM_RADIUS![5.0]);
}
#define
. Notice that the invocation of a macro is clearly distinct from an invocation of a function thanks to the postfix !
. It might be a small detail but it is important nonetheless.One additional thing you might notice is the expr
annotation after the argument. In Rust
, this is called a designator
. You can think of them as type annotation. I won’t discuss them here for the sake of brevity, but you can check them out here.
2.2.1 Multi-line revision
As you can see Rust
provides a much nicer interface for creating macros. The macro body can be scoped within a block without the pesky newline problem. This makes defining a multi-line macro much more friendly compared to C
.
Here’s a revision of the do-while
trick.
macro_rules! foo {
() => {
{
// statement 1...
// statement 2...
}
}
}