Syntax and semantics

The core metalanguage is fairly small. It has only these four types of expressions:

  • ML99_call(F, ...) invokes F with provided arguments. F must be either a functional macro identifier or an expression that evaluates to a functional macro identifier; ... must comprise a non-empty sequence of comma-separated expressions.

  • v(...) merely evaluates to its arguments, e.g. v(123) evaluates to 123.

  • ML99_abort(...) evaluates a non-empty sequence of comma-separated expressions and immediately aborts interpretation.

  • ML99_fatal(F, ...) aborts interpretation with an appropriate error message.

(Indeed, expressions do nothing unless interpreted -- i.e., they are lazy.)

All metaprograms in Metalang99 represent a sequence of these syntactic forms. The interpreter itself is called via ML99_EVAL(...).

Let's move on to examples. Consider this:

ML99_EVAL(v(123), v(~), v(***))

It simply evaluates to 123 ~ *** as expected. Now consider the mechanics of a functional macro (aka "metafunction"):

#define F_IMPL(x, y, z) v(x + y + z)

ML99_EVAL(ML99_call(F, v(1), v(2), v(3)))

It evaluates to 1 + 2 + 3. Here, v(1), v(2), v(3) is a sequence of comma-separated expressions provided to F: Metalang99 evaluates each one and applies them to F like this: F_IMPL(1, 2, 3). Notice that if we write ML99_call(F, v(1, 2, 3)), we achieve the same result because v(1, 2, 3) evaluates to 1, 2, 3 -- exactly the same arguments.

As you can see, the syntax ML99_call(F, ...) is a bit inconvenient. For the sake of proper code formatting and IDE support, the convention used by the Metalang99 standard library is to define a wrapper macro that expands to a Metalang99 call:

/// The documentation string.
#define FOO(a, b, c) ML99_call(FOO, a, b, c)
#define FOO_IMPL(a, b, c) // The implementation.

This way FOO can be conveniently called as FOO(v(1), v(2), v(3)).

Recursion

Why do we need custom syntax for invoking functional macros? Because it lets you express recursion with no hassle! Consider the following demonstrative example:

#define X_IMPL(op)        ML99_call(op, v(123))
#define CALL_X_IMPL(_123) ML99_call(X, v(ID))
#define ID_IMPL(x)        v(x)

ML99_EVAL(ML99_call(X, v(CALL_X)))

It evaluates to 123, as expected. However, without Metalang99, the expansion gets blocked:

#define X(op)        op(123)
#define CALL_X(_123) X(ID)
#define ID(x)        x

// X(ID)
X(CALL_X)

It happens because X(CALL_X) expands to CALL_X(123), which, in turn, expands to X(ID) -- the recursive macro call which is blocked by the preprocessor (see the Cloak wiki).

General macro recursion allows expressing things that were inexpressible using vanilla preprocessor macros. For example, you can leverage Cons-lists to do pretty much anything with unbounded sequences of arguments, as we shall see later. Internally, such metafunctions as ML99_listReplicate, ML99_listReverse, or ML99_listFilter are implemented by structural recursion, which would be impossible without Metalang99.

Appendix: The use of `v`

Throughout the examples, you might have noticed the extensive use of the v(...) expression. Although it may be a bit confusing for newcomers, the purpose of v(...) is pretty simple: just say the Metalang99 interpreter to evaluate your stuff inside the parentheses as-is.

Take a look at this erroneous metafunction:

#define POW2_IMPL(x) x * x

If we try to use it like this: ML99_EVAL(ML99_call(POW2, v(3))), Metalang99 would complain at us:

test.c:5:1: error: static assertion failed: "invalid term `3 * 3`"
    5 | ML99_EVAL(ML99_call(POW2, v(3)))
      | ^~~~~~~~~

The reason for this is that every single metafunction called by ML99_call (ML99_callUneval, ML99_appl) must emit a proper Metalang99 term; 3 * 3 is not a proper term according to the core language syntax. To fix the problem, just wrap x * x into v:

#define POW2_IMPL(x) v(x * x)

You can think of v as of a literal expression, like "abc" or 42 in most programming languages. However, if you want Metalang99 to evaluate an expression according to its semantics, you need not use v(...) but instead provide a computable term:

#define GEN_IMPL(n) ML99_repeat(v(n), ML99_appl(v(ML99_cat), v(_)))

// _0 _1 _2 _3 _4
ML99_EVAL(ML99_call(GEN, v(5)))

The same holds for metafunction arguments.

Last updated