This is one of the most powerful features of Lisp-based architectures.
Right now, if we use our VM's native OP_AND opcode, it is eager. If we write
["and", ["!=", "x", 0], ["=", ["/", 10, "x"], 5]], the VM evaluates both sides
before passing them to OP_AND. If x is 0, the second expression throws a
divide-by-zero error, even though the first expression was false!
To fix this, we need short-circuiting logic. But instead of cluttering our VM
with complex jumping OP_AND_THEN opcodes, we can write an AST Macro
Preprocessor.
A macro intercepts syntactic sugar like ["and-then", A, B] and rewrites it
into core primitives like ["if", A, B, false] before the static analyzer or
compiler ever see it.
1. Adding the Macro Preprocessor (engine.mjs)
Add this code to your engine.mjs file, right above the analyze function.
/ --- MACRO REGISTRY ---
const macros = {
// Short-circuit AND: ["and-then", A, B] => ["if", A, B, false]
'and-then': (args) => ['if', args[0], args[1], false],
// Short-circuit OR: ["or-else", A, B] => ["if", A, true, B]
'or-else': (args) => ['if', args[0], true, args[1]],
// Cond (Multi-branch IF): ["cond", cond1, val1, cond2, val2, fallback]
'cond': function processCond(args) {
if (args.length === 1) return args[0]; // The fallback value
if (args.length === 0) return null;
const [condition, value, ...rest] = args;
// Recursively build nested IF statements
return ['if', condition, value, processCond(rest)];
},
// Mutation Sugar: ["+=", "x", 5] => ["set", "x", ["+", "x", 5]]
'+=': (args) => ['set', args[0], ['+', args[0], args[1]]],
// While-Not Sugar: ["until", cond, body] => ["while", ["not", cond], body]
'until': (args) => ['while', ['not', args[0]], args[1]]
};
/ --- MACRO EXPANDER ---
export function expandMacros(ast) {
// Base case: primitives and symbols
if (!Array.isArray(ast) || ast.length === 0) return ast;
// Quotes are literal data, do not expand inside them
if (ast[0] === 'quote' || ast[0] === 'str') return ast;
const [op, ...args] = ast;
// If the operation is a registered macro, transform it!
if (typeof op === 'string' && macros[op]) {
const transformedAst = macros[op](args);
// Recursively expand again in case the macro emitted ANOTHER macro
return expandMacros(transformedAst);
}
// If it's a standard opcode, recursively expand all its arguments
return ast.map(child => expandMacros(child));
}
(Don't forget to export it: export { expandMacros, analyze, compile ... }).
2. Updating the Pipeline (index.html / main.mjs)
Now we update our execution pipeline to run the preprocessor as "Phase 0". By
expanding the macros before analysis, the Static Analyzer can validate the
types of the generated if blocks natively.
In your index.html (or main.mjs), update the pipeline block to look like this:
import { expandMacros, analyze, compile, StateBuffer } from './engine.mjs';
/ ...
log("Phase 0: Expanding Macros...");
const expandedAST = expandMacros(payload);
/ To see the magic, you can log the expanded AST:
log("Expanded AST:
" + JSON.stringify(expandedAST, null, 2));
log("Phase 1: Analyzing AST statically...");
analyze(expandedAST);
log("Phase 2: Compiling to Bytecode...");
const bytecode = compile(expandedAST);
/ ... dispatch to worker ...
3. Testing the Macros
Let's write a payload that utilizes our new syntax sugar. Notice how much
cleaner this is for a human to read compared to manually nesting if statements
and set operations.
const payload = [
"let", [
["x", 0],
["y", 10],
["result", ""]
],
["do",
// 1. Short-Circuiting Test (Prevents divide-by-zero crash)
["if", ["and-then", ["!=", "x", 0], ["=", ["/", "y", "x"], 5]],
["set", "result", ["str", "Math worked!"]],
["set", "result", ["str", "Avoided divide by zero!"]]
],
// 2. Syntax Sugar Test
["+=", "x", 15],
// 3. Multi-branch Cond Test
["set", "result",
["cond",
["<", "x", 10], ["str", "Too small"],
["=", "x", 15], ["str", "Exactly 15!"],
["str", "Fallback hit"]
]
],
// Return final state
["record", "x", "x", "result", "result"]
]
];
Why this is a Superpower
When you run this payload, the macro expander intercepts the and-then and
rewrites it into an if block that natively utilizes our compiler's
OP_JMP_FALSE mechanics. It intercepts the cond and rewrites it into nested if
blocks.
The VM's instruction set (ISA) doesn't grow. The worker thread doesn't get
heavier or slower. But the JSON-Lisp language becomes exponentially more
expressive. You can now build domain-specific language (DSL) features on your
Cloudflare edge nodelike ["sql-select", "users", "age", ">", 18]and compile
them down to raw loop structures before they ever hit the worker.