Skip to content
Bill Hails edited this page May 10, 2026 · 26 revisions

Macros in F♮ are a little unusual. Instead of being templates for substitution, they invoke syntax rules that guide the parser.

The best way to describe them is by example. In listutils.fn is a definition of a macro for list comprehensions called lco.

With lco you can write:

lco [ x + 1 for x in xs where x > 2, x < 20 ]

And that will expand to

list.map(fn (x) { x + 1 },
         list.filter(fn (x) { x > 2 },
                     list,filter(fn (x) { x < 20 }, xs)));

lco is defined as follows:

export macro lco: Expr internalLco;

syntax internalLco ::= "["
    exp: Expr
    "for" x: Name
    "in" xs: Expr
    filters: Syntax(whereClauses(x, xs))
"]" quote {
    map(fn (unquote x) { unquote exp }, unquote filters)
};

syntax whereClauses(x, xs) ::= empty { xs }
| "where" cond: Expr rest: Syntax(moreFilters(x, xs)) quote {
    filter(fn (unquote x) { unquote cond }, unquote rest)
};

syntax moreFilters(x, xs) ::= { xs }
| "," cond: Expr rest: Syntax(moreFilters(x, xs)) quote {
    filter(fn (unquote x) { unquote cond }, unquote rest)
};

From the top:

export macro lco: Expr internalLco;

The macro declaration merely registers lco as a trigger token, states that lco returns an expression (: Expr) and hands control to a syntax rule that does the heavy lifting. If a macro is intended to expand to a definition, use Def instead of Expr.

Syntax rules are introduced by the syntax keyword and describe a limited BNF-style grammar using quoted strings to match literal tokens. So you can read:

syntax internalLco ::= "["                  // Parse a literal "[".
    exp: Expr                               // Parse an expression and save it in `exp`.
    "for"                                   // Parse a literal "for".
    x: Name                                 // Parse a name and store it in `x`.
    "in"                                    // Literal "in".
    xs: Expr                                // Expression stored in `xs`.
    filters: Syntax(whereClauses(x, xs))    // Hand off to another syntax rule, passing it
                                            // `x` and `xs`, store the result in `filters`.
"]"                                         // literal "]" to close
quote {
    map(fn (unquote x) { unquote exp },     // Result of this syntax rule is the outer map
        unquote filters)                    // wrapping the inner filters.
};

Continuing

syntax whereClauses(x, xs) ::= empty { xs } // If there is no `where` clause just return the list.
|                                           // BNF-style "otherwise".
    "where"                                 // Match `where`.
    cond: Expr                              // Parse an expression and store it in `cond`.
    rest: Syntax(moreFilters(x, xs))        // Hand off to another syntax rule, passing `x` and `xs`.
quote {
    filter(fn (unquote x) { unquote cond }, // Result is the rest of the filters, wrapped
           unquote rest)                    // in this `cond` filter.
};

And to tie it all up, this is just a repeat of whereClauses but with "," instead of "where", and recursing on itself.

syntax moreFilters(x, xs) ::= { xs }   // `empty` token is optional.
|
    ","
    cond: Expr
    rest: Syntax(moreFilters(x, xs))
quote {
    filter(fn (unquote x) { unquote cond }, unquote rest)
};

Next: Command Line Arguments

Clone this wiki locally