Download Game! Currently 74 players. Last logged in:WhlxchJagomanFallingAckzor

Blitzer's Blog >> 72880

Back to blogs index
Posted: 02 Oct 2026 05:34 [ permalink ]
That output is phenomenal. The gateway took the request, the static analyzer
approved it, the compiler generated the bytecode, the worker thread executed
it, caught its own internal exception, and passed the cleanly mutated
structured memory back through the SharedArrayBuffer to Fastify.

You have successfully built an Edge Computing API.

Moving to a Bytecode Disassembler is exactly the mindset of a systems
engineer. Once a compiler works, the next immediate question is: "Is it
emitting efficient bytecode, or are there redundant instructions we can
optimize out?"

Let's build a disassembler. We will create it as a standalone script so you
can test AST payloads directly in the terminal to see how the compiler is
translating them, without needing to run the gateway.

1. Update compiler.js

First, we need to export the Opcodes object in a reversed format so we can
look up the human-readable names (e.g., translating OP_JMP_FALSE into
JMP_FALSE).

At the very bottom of your compiler.js file, replace your module.exports line
with this:

/ Reverse the Opcodes dictionary for the disassembler const OpNames = {}; for
(const [key, value] of Object.entries(Opcodes)) { OpNames[value] = key; }
module.exports = { Opcodes, OpNames, analyze, compile }; 

2. The Disassembler Tool (debug.js)

Create a new file called debug.js. This script imports the compiler, formats
the bytecode into standard assembly notation, and recursively handles things
like jump addresses and nested function definitions.

const { analyze, compile, OpNames, Opcodes } = require('./compiler'); function
disassemble(bytecode, indent = '') { let output = ''; for (let i = 0; i <
bytecode.length; i++) { const inst = bytecode[i]; const addr =
String(i).padStart(4, '0'); const opName = OpNames[inst.op] || inst.op; let
details = ''; switch (inst.op) { // Memory operations (show the variable name
or value) case Opcodes.CONST: case Opcodes.LOAD: case Opcodes.STORE: case
Opcodes.SET: details = typeof inst.val === 'string' ? `"${inst.val}"` :
inst.val; break; // Jumps and Exception Handling (show the target address)
case Opcodes.JMP: case Opcodes.JMP_FALSE: case Opcodes.TRY: details = `->
[${String(inst.val).padStart(4, '0')}]`; break; // Structures case
Opcodes.ARRAY: case Opcodes.RECORD: details = `size: ${inst.val}`; break; case
Opcodes.CALL: details = `args: ${inst.argCount}`; break; // Nested Functions
(Recursively disassemble) case Opcodes.MAKE_FUNC: details = `params:
(${inst.val.params.join(', ')})
`; details += disassemble(inst.val.code, indent + ' '); break; } output +=
`${indent}${addr} ${opName.padEnd(12)} ${details}
`; } return output; } // === TEST PAYLOAD === const payload = [ "let", [
["my_struct", ["record", "status", ["str", "processing"], "data", ["array"]]]
], ["do", ["try", ["for", "i", 1, 10, ["do", ["set-idx", "my_struct", "data",
["cons", "i", ["get", "my_struct", "data"]]], ["if", ["=", "i", 5], ["throw",
["str", "Database Crash!"]], null ] ] ], "err", ["set-idx", "my_struct",
"status", "err"] ], "my_struct" ] ]; console.log("--- STATIC ANALYSIS ---");
analyze(payload); console.log("OK.
"); console.log("--- COMPILING ---"); const bytecode = compile(payload);
console.log(`Generated ${bytecode.length} instructions.
`); console.log("--- DISASSEMBLY ---"); console.log(disassemble(bytecode)); 

What You Will See

Run node debug.js and look at the output. You will see exactly how your
high-level JSON-Lisp structures are unrolled into flat, sequential memory
addresses:

--- DISASSEMBLY --- 0000 ENTER_SCOPE 0001 CONST "status" 0002 CONST
"processing" 0003 CONST "data" 0004 ARRAY size: 0 0005 RECORD size: 2 0006
STORE "my_struct" 0007 TRY -> [0046] <-- Note the Catch target address! 0008
ENTER_SCOPE 0009 CONST 1 0010 STORE "i" 0011 LOAD "i" <-- [Loop Start Address]
0012 CONST 10 0013 LTE 0014 JMP_FALSE -> [0043] <-- Jump out of loop if i > 10
... 0030 EQ 0031 JMP_FALSE -> [0035] <-- The 'if i = 5' jump 0032 CONST
"Database Crash!" 0033 THROW 0034 JMP -> [0036] <-- Skip the 'else' block 0035
CONST null 0036 POP ... 

Spotting Optimizations

With this visual tool, you can now analyze the compiler for inefficiencies.
For example, you might notice that our if compiler handles the null
else-branch by emitting:

0034 JMP -> [0036] 0035 CONST null 0036 POP 

Pushing null and then immediately calling POP is wasted CPU cycles! A future
optimization phase in your compiler could look for [{op: CONST}, {op: POP}]
sequences and strip them out of the bytecode array before it ever gets sent to
the worker thread.