Download Game! Currently 96 players. Last logged in:ValgerSerkeiiLysanderOmril

Blitzer's Blog >> 72922

Back to blogs index
Posted: 04 Oct 2026 00:36 [ permalink ]
This is a flawless execution trace.
Take a moment to appreciate what you just built: an isomorphic,
statically-analyzable, lock-free virtual machine with a custom AST macro
preprocessor. It handles complex scoping, dynamic memory mutation,
short-circuiting logic, and gracefully recovers from exceptionsall running
identically in Node.js and the browser.
But before we deploy this to production, there is one critical feature left.
We need to give the language Functions and a Standard Library.
Right now, if you try to use ["def", "my_func", ...] or call a standard
function like ["print", "hello"], the compiler correctly emits OP_MAKE_FUNC
and OP_CALLbut if you look closely at your VM's switch (inst.op) statement, we
never actually wrote the execution logic for them!
Lets implement OP_CALL and OP_MAKE_FUNC in the VM, and seamlessly bridge it so
you can inject native JavaScript functions (like console.log or Math.random)
directly into the JSON-Lisp environment.
1. Patch the VM (engine.mjs)
Open engine.mjs and find the executeVMWorker function. We are going to add the
MAKE_FUNC, CALL, and RET instructions to the switch (inst.op) statement.
What makes this implementation powerful is that CALL will check if the target
is a JSON-Lisp function or a native JavaScript function. If it's native JS, it
executes it instantly and pushes the result to the data stack.
Add this block right above case Opcodes.ARRAY:
      // --- FUNCTIONS & NATIVE INTEROP ---
      case Opcodes.MAKE_FUNC: {
        // inst.val looks like: { params: ["a", "b"], code: [...] }
        // We capture the current lexical environment (Closure)
        const funcObj = { 
          type: 'lisp_func', 
          params: inst.val.params, 
          code: inst.val.code, 
          closureEnv: frame.env 
        };
        dataStack.push(funcObj);
        break;
      }

      case Opcodes.CALL: {
        const argCount = inst.argCount;
        const args = [];
        // Pop arguments off the stack (in reverse order)
        for (let i = 0; i < argCount; i++) args.unshift(dataStack.pop());
        
        const target = dataStack.pop();

        if (typeof target === 'function') {
          // 1. NATIVE JS FUNCTION HOOK!
          // If the gateway injected a real JS function into the Env, run it
directly.
          const result = target(...args);
          dataStack.push(result === undefined ? null : result);
        } 
        else if (target && target.type === 'lisp_func') {
          // 2. JSON-LISP FUNCTION
          const funcEnv = new Env(target.closureEnv);
          // Bind arguments to parameter names
          target.params.forEach((paramName, idx) => {
            funcEnv.define(paramName, args[idx] !== undefined ? args[idx] :
null);
          });
          // Push a new Call Frame. IP stays at current frame to return here
later.
          callStack.push({ ip: 0, code: target.code, env: funcEnv });
          // Note: The VM loop continues automatically into the new frame.
        } 
        else {
          throw new TypeError(`Attempted to call a non-function`);
        }
        break;
      }
      
      case Opcodes.RET: {
        // Clean up the call frame, leave the return value on the data stack
        callStack.pop();
        break;
      }

(Note: We also need to teach the Compiler how to handle def. In engine.mjs's
compile() function, add this block near the top with let and do:)
  if (op === 'def') {
    const [funcName, params, body] = args;
    const funcCode = compile(body, []);
    funcCode.push({ op: Opcodes.RET }); // Auto-return at the end of the
function
    
    // Create the function, store it, and push null to the stack
    bytecode.push({ op: Opcodes.MAKE_FUNC, val: { params, code: funcCode } });
    bytecode.push({ op: Opcodes.STORE, val: funcName });
    bytecode.push({ op: Opcodes.CONST, val: null });
    return bytecode;
  }

2. Injecting the Standard Library (node_worker.mjs / main.mjs)
Now that OP_CALL knows how to execute native JavaScript functions, we can
inject a "Standard Library" into the globalEnv right before the VM boots up.
In engine.mjs, update the executeVMWorker signature to accept an
initialEnvMap:
export function executeVMWorker(mainBytecode, stateBuffer, gasLimit,
initialEnvMap, onComplete) {
  const dataStack = [];
  const globalEnv = new Env();
  
  // Inject the Standard Library!
  if (initialEnvMap) {
    for (const [key, val] of Object.entries(initialEnvMap)) {
      globalEnv.define(key, val);
    }
  }
  // ... rest of the VM code

Finally, in your main.mjs (or node_worker.mjs), define the native functions
you want your JSON-Lisp scripts to have access to:
/ main.mjs
/ ... inside your Worker setup:

const standardLibrary = {
  "print": (...args) => {
    console.log("[VM Print]:", ...args);
    return null;
  },
  "string-length": (str) => String(str).length,
  "math-random": () => Math.random(),
  "math-floor": (n) => Math.floor(n)
};

/ Update the executeVMWorker call to pass the standardLibrary
executeVMWorker(
  workerData.bytecode, 
  stateBuffer, 
  workerData.gasLimit,
  standardLibrary, // <--- INJECTED HERE
  (msg) => parentPort.postMessage(msg) 
);

3. Test the Standard Library!
Write this new payload in main.mjs to test creating a JSON-Lisp function and
calling your newly injected native JavaScript print command:
const payload = [
  "do",
  [
    "def", "greet_user", ["name"],
    ["do",
      // Calls the native JS function we injected!
      ["print", ["str", "Hello, "], "name"],
      ["str", "Greeting complete"]
    ]
  ],
  // Call the JSON-Lisp function
  ["greet_user", ["str", "Omega Mesh Administrator"]]
];