MetaScript
Beta
METAPROGRAMMING

Compiler power, in user-land

MetaScript is an open compiler. It allows source code to be rewritten at compile time through metaprogramming — reaching the compiler's deeper layers without modifying its source, and giving developers limitless possibilities.

Three mechanisms expose the compiler:

  • @comptime blocks evaluate code at compile time; the result becomes a literal in the AST.
  • Custom macros (the macro keyword) walk AST nodes and return new AST.
  • Backend directives like @include, @link, @compile give per-target control over what gets emitted.

All three are written in plain MetaScript — no separate macro DSL — using the compiler's own typed Node from std/meta. Every Node value lives only at compile time and is erased before codegen, so macros never leak into the runtime.

@comptime blocks

Evaluate a block at compile time. The return value is inlined into the AST as a literal — and that's the load-bearing feature. Compute-heavy work and side effects during compilation become baked-in constants, so the runtime stays clean.

const SQUARES = @comptime {
    const items: number[] = [];
    let i = 0;
    while (i < 10) { items.push(i * i); i = i + 1; }
    return items;
};
// → [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Anything expressible in plain MetaScript can run inside @comptime — loops, computation, building data structures. The return value (number, string, boolean, array, object, or null) maps to the corresponding literal AST node, and from that point on it's just a constant.

Custom macros

A macro is a function defined with the macro keyword. It receives an AST node, builds new AST, and returns it. The returned node replaces the call site in the compilation pipeline.

import { Node, NodeKind } from "std/meta";

macro double(x: Node): Node {
    return {
        kind: NodeKind.BinaryExpr,
        line: x.line,
        column: x.column,
        operator: "+",
        left: x,
        right: x,
    };
}

const y = double(21);   // → 21 + 21 = 42 (the call expands at compile time)

Ways to call a macro

FormExampleMacro receives
Bare call (most common)double(21)the argument node
Decorator@logger class Foo {}the class declaration

@name is reserved for two cases: decorator position before a declaration and built-in directives (@emit, @include, etc.). Everywhere else, macros are called as regular functions: name(args).

Cross-module macros

Macros can be exported and imported across modules:

// macros.ms
export macro double(x: Node): Node {
    return { kind: NodeKind.BinaryExpr, line: x.line, column: x.column,
             operator: "+", left: x, right: x };
}

// app.ms
import { double } from "./macros";
const y = double(21);

quote { ... } templates

Building AST manually is verbose. quote { } parses an expression as an AST template; ${expr} interpolates computed Node values into the template.

import { Node } from "std/meta";

macro addOne(x: Node): Node {
    return quote { ${x} + 1 };
}

const z = addOne(41);   // → 42

quote produces a Node (compile-time only, same as JSX and createNode). Anything quote does can be done by hand with createNode, but quote is far more readable for non-trivial templates.

Why a macro instead of a function?

Macros see the AST, not just runtime values. A function receiving "Count: " + String(count()) only sees the resulting string. A macro receives a BinaryExpr containing a CallExpr to count() — it can detect that count() is reactive and selectively wrap only the reactive parts. This is the kind of analysis that makes JSX → DOM compile away to zero-overhead makeElement / bindProps / insertChild calls.

Backend directives

Directives give per-target control over what the compiler emits.

@include("mylib.h");           // C: #include in emitted output
@compile("mylib.c");           // C: also compile this file alongside
@link("libcrypto.a");          // C: link against this static lib
@passC("-DDEBUG=1");           // C: extra cc flag
@passL("-lssl");               // C: extra ld flag

For the deep dive — node serialisation, the Raiser bridge, the four-phase implementation plan — see LANG-METAPROGRAMMING.md in the compiler repo.

Next steps

  • Type System — how the checker infers types around macro output
  • Compile Targets — what each backend can do with the AST a macro returns