Work in Progress
You are viewing unfinished draft material right now.
Macros in Detail
In this document, we provide a structured introduction to writing stateful macros, and the required details of the evaluation process. We assume that you read the setup and jsx syntax page, but require no further knowledge about Macromania.
On the setup page, we have already seen how to define and invoke basic macros:
import { evaluate } from "macromania";
import type { Children, Expression } from "macromania";
import { assertEquals } from "@std/assert";
function Greet(
{ hype, children }: { hype: number; children?: Children },
): Expression {
return (
<>
Hello, <xs x={children} />
{"!".repeat(hype)}
</>
);
}
assertEquals(
await evaluate(
<Greet hype={1 + 2}>world</Greet>,
),
"Hello, world!!!",
);While this is already pretty neat, Macromania really starts to shine when it comes to stateful macros. While we could simply mutate some global state to have our macros behave differently on different invocations, Macromania offers more sophisticated tools.
Basics of Mutable State
You can ask Macromania to manage some mutable state for you with the Context.createState function. You pass it a function that provides an initial state, and you get back a getter and a setter for that state. The state can be of an arbitrary type.
import { Context } from "macromania";
/*
* Create getter and setter for some state of type `number`,
* initialised to zero.
*/
const [getCount, setCount] = Context.createState<
number // type of the state
>(
() => 0, // function producing the initial state
); You cannot use the getter and setter directly, because the state is not global. Instead, Macromania creates a new, independent version of this state for every call to evaluate. In order to access and mutate the state, we need a new kind of intrinsic: the <effect/> intrinsic.
function Count(): Expression {
return (
<effect
/*
* The <effect/> intrinsic evaluates a function to
* create an expression, and passes to that function
* an evaluation `Context`.
*/
fun={(ctx: Context) => {
/*
* `ctx` manages all evaluation-wide state, and
* provides it to the getter and the setter.
*/
const oldCount = getCount(ctx);
setCount(ctx, oldCount + 1);
return `${oldCount}`;
}}
/>
);
}
assertEquals(
await evaluate(
<>
<Count />
<Count />
<Count />
</>,
),
"012",
);
/* New evaluation, independent state: */
assertEquals(await evaluate(<Count />), "0"); The <effect/> intrinsic accepts no children, but it has a mandatory prop fun. The fun prop is a function (sync and async both work) that maps a Context object to an Expression. The getter and setter returned by Context.createState take a Context as their first argument — the Context that manages all state for the current evaluation process.
When you call evaluate, Macromania sets up a fresh Context for this particular evaluation process. When Macromania needs to evaluate an <effect/> intrinsic, it calls the fun prop with the context as argument. Macromania then effectively replaces the <effect/> expression with the return value of fun — and resumes evaluation with that new expression.
The first time you use any getter returned by Context.createState, the context creates the initial state by calling the initial argument to Context.createState. If you use the corresponding setter before your first usage of the getter, then initial is never called at all.
In additoin to keeping state scoped to a single evaluation process, this system also keeps state private and independent: many packages might define all different kinds of state, but they cannot directly access each others’ states. Of course, packages can choose to expose access to their state simply by exporting appropriate functions:
/**
* Returns whether the current count is even.
*/
export function isCountEven(ctx: Context): boolean {
return getCount(ctx) % 2 === 0;
}In this example, the package which maintains the count has unrestricted access, but any other packages cannot interact with the count beyond querying whether it is even or not. More generally speaking, packages can expose meaningful APIs for interoperability with other packages, but without ever having to worry about conflicts or interference.
The Context object does not only manage user-defined state, it also has some further useful methods. We leave those for another piece of documentation. Good to know at this point, however: if you ever need to asynchronously initialise some state, the Context.createAsyncState function has you covered.
Delaying Evaluation
Consider the following pair of macros for defining concepts and for referencing them in HTML (halting evaluation with an error when trying to reference an undefined concept):
const [getDefs, _setDefs] = Context.createState<
Set<string>
>(
() => new Set(),
);
function Def({ id }: { id: string }): Expression {
return (
<effect
fun={(ctx) => {
getDefs(ctx).add(id);
return `<dfn id="${id}">${id}</dfn>`;
}}
/>
);
}
function Ref({ id }: { id: string }): Expression {
return (
<effect
fun={(ctx) => {
if (getDefs(ctx).has(id)) {
return `<a href="#${id}">${id}</a>`;
} else {
/* Halts evaluation with an error. */
return ctx.halt();
}
}}
/>
);
}Not immensly sophisticated, but still pretty neat.
assertEquals(
await evaluate(
<>
A <Def id="squirrel" /> is essentially a mouse
with a bushy tail. I saw a <Ref id="squirrel" /> today.
</>,
),
`A <dfn id="squirrel">squirrel</dfn> is essentially a mouse with a bushy tail. I saw a <a href="#squirrel">squirrel</a> today.`,
);
assertEquals(
await evaluate(<>I saw a <Ref id="glorb"/> today.</>),
null, // "glorb" is undefined, so evaluation fails.
);Unfortunately, these macros ony work when a definition precedes its reference(s). You cannot do forward references:
assertEquals(
await evaluate(<>
I saw a <Ref id="glorb"/> today.
A <Def id="glorb"/> is essentially a mouse but weird.
</>),
null, // "glorb" undefined when evaluating the `Ref` macro :(
); The Ref macro cannot tell in advance whether any given id will be added in the future. Hence, Macromania offers an alternative: the Ref macro can report to the evaluation process that it cannot be evaluated yet and should be retried later. The <effect/> intrinsic offers this feature: when its fun prop returns null instead of an Expression, Macromania simply continues evaluation elsewhere, and tries again at a later time.
Before we use this feature to fix our Ref macro, first a few more details. When an <effect/> intrinsic keeps returning null, evaluation can never finish. After two separate evaluation rounds where no macro makes any progress, Macromania gives up (and logs all offending macro invocations to the terminal).
assertEquals(
await evaluate(<effect fun={(_ctx) => null}/>),
null,
); The mustMakeProgress method of the Context object lets you query whether Macromania will give up the evaluation process if no progress will be made by any macro in the current evaluation round. This is how we can fix the Ref macro: it can delay its evaluation until mustMakeProgress returns true. By that time, all definitions will (hoefully) have been evaluated.
function Ref2({ id }: { id: string }): Expression {
return (
<effect
fun={(ctx) => {
if (getDefs(ctx).has(id)) {
return `<a href="#${id}">${id}</a>`;
} else {
if (ctx.mustMakeProgress()) {
/* We give up. */
return ctx.halt();
} else {
/* We hope the Def is added later. */
return null;
}
}
}}
/>
);
}
assertEquals(
await evaluate(<>
I saw a <Ref2 id="glorb"/> today.
A <Def id="glorb"/> is essentially a mouse but weird.
</>),
`I saw a <a href="#glorb">glorb</a> today. A <dfn id="glorb">glorb</dfn> is essentially a mouse but weird.`,
);Nested Evaluation
Some macros need to evalaute a subexpression into a string and then operate on that string. Imagine, for example, a markdown macro which wants to interpret its evaluated children as markdown and then emit the corresponding html. If you used the <effect/> intrinsic to call evaluate children, you would run into the problem that the children would be evaluated using a completely fresh evaluation state. You would also run into the problem that nested evaluate deliberately throw an error, because they are always a bad idea.
Instead, there is an intrinsic to get the job done: the <map/> intrinsic evaluates its children, and then calls a fun prop with the evaluation Context and with the string to which the children evaluated. The fun then returns an other expression; the whole intrinsic evaluates to that expression.
Here is an example macro that turns its children into UPPER CASE:
function Yell({ children }: { children: Children }): Expression {
return (
<map fun={(_ctx, evaled) => {
return evaled.toUpperCase();
}}><xs x={children} /></map>
);
}
assertEquals(
await evaluate(<><Yell>hello</Yell>, world!</>),
`HELLO, world!`,
); The fun prop of a <map/> intrinsic can be async. Unlike with the <effect/> intrinsic, however, the function cannot return null. If you need to delay the result of a <map/> intrinsic, simply have the fun return an appropriate <effect/> intrinsic.
Framed Evaluation
Occasionally, you may want to evaluate a subexpression, but to adjust some state before and after the subexpression is evaluated. Given that the subexpression might be an <effect/> macro with delayed evaluation, the framing state changes might be performed multiple times. The <lifecycle/> is designed to handle this situation. Its pre and post props are functions taking a Context and returing void (or a Promise<void> for async state manipulation). The pre function is called each time before attempting to evaluate the child expressions, and the post function is called each time after attempting to evaluate the child expressions.
In the following example, we write a macro for nicely indenting nested lists of items.
const [getIndentation, setIndentation] = Context.createState<
number
>(
() => -1,
);
function List({ children }: { children?: Children }): Expression {
return <effect fun={(ctx) => {
return (
<lifecycle
pre={(ctx) => {
// Increment the indentation before evaluating the items.
setIndentation(ctx, getIndentation(ctx) + 1);
}}
post={(ctx) => {
// Decrement indentation after evaluating the items.
setIndentation(ctx, getIndentation(ctx) - 1);
}}
>
<xs x={children}/>
</lifecycle>
);
}} />;
}
function Item({ children }: { children?: Children }): Expression {
return <effect fun={(ctx) => {
return <>{" ".repeat(getIndentation(ctx))}- <xs x={children}/>{"\n"}</>
}} />;
}
assertEquals(
await evaluate(<List>
<Item>strings</Item>
<List>
<Item>violins</Item>
<Item>violas</Item>
</List>
<Item>woodwinds</Item>
</List>),
`- strings
- violins
- violas
- woodwinds
`,
);Wrapping Up
You know know how to write stateful macros with Macromania. Yay!
If you want to learn even more about macro writing, we recommend reading our guide to advanced macros. For a more practical next step, you could follow the tutorial for using Macromania as a simple static site generator. Or perhaps you could simply open up a text editor and start building something cool.