Because we have already built a robust set of jump and scope opcodes (JMP,
JMP_FALSE, ENTER_SCOPE, EXIT_SCOPE), we do not need to change the VM execution
loop at all.
Implementing structured loops is entirely a compiler trick. The compiler reads
the high-level AST and dynamically generates the raw, unstructured jump
addresses that the VM actually executes.
Here is the exact code to add to your compile() function to support while,
repeat, and for loops.
The Stack Alignment Rule
Before writing the compiler logic, we must enforce a critical rule of stack
machines: Every expression must leave exactly one value on the data stack.
Because a loop executes its body multiple times, it would flood the data stack
with results. To prevent memory leaks, our compiler will emit an OP_POP after
every body execution, and emit an OP_CONST of null when the loop fully
terminates.
Expanding the Compiler
Add this block into your compile() function, right near where you handle if
and do:
// 1. While Loop: ["while", condition, body]
if (op === 'while') {
const [cond, body] = args;
const loopStart = bytecode.length; // Mark the address to jump back to
compile(cond, bytecode, options); // Push condition result
const jmpFalseInst = { op: Opcodes.JMP_FALSE, val: null };
bytecode.push(jmpFalseInst); // Exit loop if false
compile(body, bytecode, options);
bytecode.push({ op: Opcodes.POP }); // Discard body result
bytecode.push({ op: Opcodes.JMP, val: loopStart }); // Jump to beginning
jmpFalseInst.val = bytecode.length; // Patch the exit address
bytecode.push({ op: Opcodes.CONST, val: null }); // Loop expression yields
null
return bytecode;
}
// 2. Repeat-Until Loop (Pascal Style): ["repeat", body, condition]
// Runs at least once, loops until condition becomes TRUE.
if (op === 'repeat') {
const [body, cond] = args;
const loopStart = bytecode.length;
compile(body, bytecode, options);
bytecode.push({ op: Opcodes.POP }); // Discard body result
compile(cond, bytecode, options);
bytecode.push({ op: Opcodes.JMP_FALSE, val: loopStart }); // Jump back if
FALSE
bytecode.push({ op: Opcodes.CONST, val: null });
return bytecode;
}
// 3. For Loop: ["for", "i", startExpr, endExpr, body]
if (op === 'for') {
const [varName, startExpr, endExpr, body] = args;
bytecode.push({ op: Opcodes.ENTER_SCOPE }); // Isolate loop variables
// Initialize the counter variable
compile(startExpr, bytecode, options);
bytecode.push({ op: Opcodes.STORE, val: varName });
const loopStart = bytecode.length;
// Condition: i <= endExpr
bytecode.push({ op: Opcodes.LOAD, val: varName });
compile(endExpr, bytecode, options);
bytecode.push({ op: Opcodes.LTE });
const jmpFalseInst = { op: Opcodes.JMP_FALSE, val: null };
bytecode.push(jmpFalseInst);
// Body execution
compile(body, bytecode, options);
bytecode.push({ op: Opcodes.POP });
// Increment: i = i + 1
bytecode.push({ op: Opcodes.LOAD, val: varName });
bytecode.push({ op: Opcodes.CONST, val: 1 });
bytecode.push({ op: Opcodes.ADD });
bytecode.push({ op: Opcodes.SET, val: varName });
bytecode.push({ op: Opcodes.POP }); // SET pushes the assigned value, pop
it
// Loop back and patch
bytecode.push({ op: Opcodes.JMP, val: loopStart });
jmpFalseInst.val = bytecode.length;
bytecode.push({ op: Opcodes.EXIT_SCOPE }); // Destroy loop variables
bytecode.push({ op: Opcodes.CONST, val: null });
return bytecode;
}
How this elevates the JSON-Lisp
You no longer have to rely solely on deep recursion (which can be hard for
humans to reason about) to execute repetitive tasks. You can now write
standard algorithmic code.
For example, look at how clean a payload to sum an array of numbers becomes
using the new for loop:
[
"let", [
["total", 0],
["max_count", 100]
],
[
"do",
["for", "i", 1, "max_count",
["set", "total", ["+", "total", "i"]]
],
"total"
]
]
When the gateway compiles this payload, the for loop seamlessly expands into
18 highly-optimized bytecode instructions, wrapped in a local block scope so
the variable "i" automatically disappears from memory the moment the loop
finishes.