The output you are seeing is a classic stack corruption!
Look closely at the final JSON object:
{
"0": "MASKED_",
"1024": 255,
"concatenated": "MASKED_",
"first_char": "J"
}
Notice how 1024 and 255 bled into the key-value positions, and "signature",
"length", and "type" were completely erased?
This happens when the Virtual Machine encounters an opcode it doesn't
recognize (or an opcode that was accidentally deleted). When the VM hit
OP_BIT_AND and OP_ADD, it fell through the switch statement, doing nothing.
Because they did nothing, they left their raw operands (1024, 255, 0, and
"MASKED_") stranded on the data stack.
When OP_RECORD finally executed at the end, it reached blindly into the stack
and grabbed those stranded operands instead of the real keys and values,
corrupting the memory structure.
This almost certainly means that when you pasted the new math block into
engine.mjs, the old math operations (ADD, SUB, etc.) were accidentally
deleted, or OP_BIT_AND didn't correctly map.
Let's freeze the completely correct, final execution block for the Virtual
Machine.
The Fix: Freeze the VM Switch Statement
Open engine.mjs and replace the entire Math/Logic/Introspection section inside
your VM's switch (inst.op) statement with this definitive block. It includes
all the original math, the new bitwise operators, and the new introspections,
ensuring nothing falls through the cracks:
// --- INTROSPECTION & SLICING ---
case Opcodes.TYPEOF: {
const val = dataStack.pop();
if (val === null) dataStack.push("null");
else if (Array.isArray(val)) dataStack.push("array");
else if (typeof val === 'object' && val.type === 'lisp_func')
dataStack.push("function");
else if (typeof val === 'object') dataStack.push("record");
else dataStack.push(typeof val); // "string", "number", "boolean"
break;
}
case Opcodes.LEN: {
const val = dataStack.pop();
if (val && typeof val.length === 'number') dataStack.push(val.length);
else dataStack.push(0);
break;
}
case Opcodes.SLICE: {
const end = dataStack.pop();
const start = dataStack.pop();
const target = dataStack.pop();
if (target && typeof target.slice === 'function') dataStack.push(target
.slice(start, end));
else dataStack.push(null);
break;
}
// --- MATH & LOGIC ---
case Opcodes.ADD: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() + r); break; }
case Opcodes.SUB: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() - r); break; }
case Opcodes.MUL: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() * r); break; }
case Opcodes.DIV: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() / r); break; }
case Opcodes.MOD: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() % r); break; }
case Opcodes.EQ: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() === r); break; }
case Opcodes.NEQ: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() !== r); break; }
case Opcodes.LT: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() < r); break; }
case Opcodes.LTE: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() <= r); break; }
case Opcodes.GT: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() > r); break; }
case Opcodes.GTE: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() >= r); break; }
case Opcodes.AND: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() && r); break; }
case Opcodes.OR: { const r = dataStack.pop(); dataStack.push(dataStack.p
op() || r); break; }
case Opcodes.NOT: { dataStack.push(!dataStack.pop()); break; }
// --- BITWISE MATH ---
case Opcodes.BIT_AND: { const r = dataStack.pop(); dataStack.push(dataSta
ck.pop() & r); break; }
case Opcodes.BIT_OR: { const r = dataStack.pop(); dataStack.push(dataSta
ck.pop() | r); break; }
case Opcodes.BIT_XOR: { const r = dataStack.pop(); dataStack.push(dataSta
ck.pop() ^ r); break; }
case Opcodes.BIT_SHL: { const r = dataStack.pop(); dataStack.push(dataSta
ck.pop() << r); break; }
case Opcodes.BIT_SHR: { const r = dataStack.pop(); dataStack.push(dataSta
ck.pop() >> r); break; }
Run node main.mjs one more time. With the math and bitwise opcodes properly
restored, they will pop their operands cleanly, pushing only the final string
"MASKED_0" to the stack. The OP_RECORD command will map it flawlessly,
returning your perfectly sized parsing structure!