Skip to content

Expanding Macros

GoogleFeud edited this page Sep 19, 2023 · 2 revisions

Expanding

Expanding macros

Every macro expands into the code that it contains. How it'll expand depends entirely on how the macro is used. Javascript has 3 main constructs: Expression, ExpressionStatement, and Statement. Since macro calls are plain function calls, macros can never be used as a statement.

Expanded macros are always hygienic!

ExpressionStatement

If a macro is an ExpressionStatement, then it's going to be "flattened" - the macro call will literally be replaced by the macro body, but all the newly declared variables will have their names changed to a unique name.

function $map<T, R>(arr: Array<T>, cb: (el: T) => R) : Array<R> {
    const array = arr; 
    const res = [];
    for (let i=0; i < array.length; i++) {
       res.push(cb(array[i]));
    }
    return res;
}
$map!([1, 2, 3], (num) => num * 2); // This is an ExpressionStatement
const array_1 = [1, 2, 3];
const res_1 = [];
for (let i_1 = 0; i_1 < array_1.length; i_1++) {
    res_1.push(((num) => num * 2)(array_1[i_1]));
}
array_1;

You may have noticed that the return statement got omitted from the final result. return will be removed only if the macro is running in the global scope. Anywhere else and return will be there.

Expression

Expanding inside an expression can do two different things depending on what the macro expands to.

Single expression

If the macro expands to a single expression, then the macro call is directly replaced with the expression.

function $push(array: Array<number>, ...nums: Array<number>) : number {
    return +["()", (nums: number) => array.push(nums)];
}
const arr: Array<number> = [];
const newSize = $push!(arr, 1, 2, 3);
const arr = [];
const newSize = (arr.push(1), arr.push(2), arr.push(3));

return gets removed if the macro is used as an expression.

Multiple expressions

If the macro expands to multiple expressions or has a statement inside it's body, then the body is wrapped inside an IIFE (Immediately Invoked function expression) and the last expression gets returned automatically.

function $push(array: Array<number>, ...nums: Array<number>) : number {
    +[(nums: number) => array.push(nums)];
}
const arr: Array<number> = [];
const newSize = $push!(arr, 1, 2, 3);
const arr = [];
const newSize = (() => {
    arr.push(1)
    arr.push(2)
    return arr.push(3);
})();
Escaping the IIFE

If you want part of the code to be run outside of the IIFE (for example you want to return, or yield, etc.) you can use the $$escape built-in macro. For example, here's a fully working macro that expands to a completely normal if statement, but it can be used as an expression:

function $if<T>(comparison: any, then: () => T, _else?: () => T) {
    return $$escape!(() => {
        var val;
        if ($$kindof!(_else) === ts.SyntaxKind.ArrowFunction) {
            if (comparison) {
                val = $$escape!(then);
            } else {
                val = $$escape!(_else!);
            }
        } else {
            if (comparison) {
                val = $$escape!(then);
            }
        }
        return val;
    });
}
const variable: number = 54;
console.log($if!<string>(1 === variable, () => {
    console.log("variable is 1");
    return "A";
}, () => {
    console.log("variable is not 1");
    return "B";
}));
const variable = 54;
var val_1;
if (1 === variable) {
    // Do something...
    console.log("variable is 1");
    val_1 = "A";
}
else {
    console.log("variable is not 1");
    val_1 = "B";
}
console.log(val_1);

Macro variables

You can define macro variables inside macros, which save an expression and expand to that same expression when they are referenced. They can be used to make your macros more readable:

function $test<T>(value: T) {
    const $type = $$typeToString!<T>();
    if ($type === "string") return "Value is a string.";
    else if ($type === "number") return "Value is a number.";
    else if ($type === "symbol") return "Value is a symbol.";
    else if ($type === "undefined" || $type === "null") return "Value is undefined / null.";
    else return "Value is an object.";
}
const a = $test!(null);
const c = $test!(123);
const f = $test!({value: 123});
const a = "Value is undefined / null.";
const c = "Value is a number.";
const f = "Value is an object.";