Download Game! Currently 54 players and visitors. Last logged in:ArasakaMahjongZenickRauskis

Blitzer's Blog >> 72226

Back to blogs index
Posted: 10 Sep 2026 06:05 [ permalink ]
When using JavaScripts prototype chain (Object.create) to model lexical
environments, you run into a fundamental feature of the language: property
shadowing.
If a variable x lives in the parent closure environment, and you execute
frame.env['x'] = 20 in the child environment, JavaScript does not mutate the
parent's x. Instead, it creates a brand new property named x directly on the
child object, shadowing the outer variable.
To properly implement a SET opcode (reassignment) as distinct from STORE
(initialization), the VM must manually traverse the prototype chain to find
exactly which environment owns the variable before mutating it.
1. Differentiating STORE and SET
First, establish clear semantics in your compiler:
 * STORE creates a new binding in the current local scope (used for function
parameters and new variable declarations).
 * SET modifies an existing binding, traversing up the scope chain until it
finds it.
2. Adapting the Compiler
We add a new set form to our JSON-Lisp AST: ["set", "x", ["+", "x", 1]].
const Opcodes = Object.assign(Opcodes || {}, {
  SET: 'OP_SET' // Pops value, walks scope chain, mutates existing variable
});

function compile(ast, bytecode = []) {
  // ... primitive and base cases ...
  const [op, ...args] = ast;

  if (op === 'set') {
    const [varName, valueExpr] = args;
    
    // Compile the expression to leave the new value on the data stack
    compile(valueExpr, bytecode);
    
    // Emit the SET instruction with the variable name
    bytecode.push({ op: Opcodes.SET, val: varName });
    return bytecode;
  }
  
  // ... rest of compile logic (def, if, STORE, CALL) ...
}

3. Implementing Prototype Traversal in the VM
Inside the execution loop, we handle OP_SET by utilizing Object.prototype.hasOw
nProperty and Object.getPrototypeOf. This guarantees we only mutate the
specific object in the chain where the variable was originally declared.
      case Opcodes.SET: {
        const varName = inst.val;
        const newValue = dataStack.pop();
        let targetEnv = frame.env;

        // Traverse upwards through the lexical closures
        while (targetEnv !== null) {
          // Check if THIS specific environment object owns the property
          if (Object.prototype.hasOwnProperty.call(targetEnv, varName)) {
            targetEnv[varName] = newValue;
            break;
          }
          // Move up the prototype chain to the parent environment
          targetEnv = Object.getPrototypeOf(targetEnv);
        }

        // If we reached the end of the chain (null) without finding it
        if (targetEnv === null) {
          throw new ReferenceError(`Assignment to undeclared variable:
${varName}`);
        }
        break;
      }

How This Behaves in Practice
Consider a counter closure:
[
  "def", "make_counter", [],
  [
    "def", "count", [],
    [
      "set", "x", ["+", "x", 1]
    ]
  ]
]

(Assuming x was initialized in make_counter's scope via a let block or
similar).
When count is called:
 * OP_SET pops the new incremented value off the data stack.
 * It looks at count's local frame.env. hasOwnProperty('x') returns false
(because x is in the parent).
 * It steps up to the closureEnv of make_counter.
 * hasOwnProperty('x') returns true.
 * It mutates targetEnv['x'] = newValue, correctly maintaining the stateful
closure without polluting the local frame or throwing a reference error.