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:
@comptimeblocks evaluate code at compile time; the result becomes a literal in the AST.- Custom macros (the
macrokeyword) walk AST nodes and return new AST. - Backend directives like
@include,@link,@compilegive 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
| Form | Example | Macro 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); // → 42quote 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 flagFor 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