No registers, just a stack
Unlike RISC-V, wasm is a stack machine: i32.const 2 pushes 2, i32.add pops two values and pushes their sum. Locals hold named values. This makes the bytecode tiny and easy to validate — and easy to interpret.
An interpreter
function runStack(code, locals = []) {
const stack = [];
const L = [...locals];
const pop = () => {
if (!stack.length) throw new Error("Stack underflow");
return stack.pop();
};
for (const line of code) {
const [op, arg] = line.split(" ");
switch (op) {
case "i32.const": stack.push(Number(arg) | 0); break;
case "local.get": stack.push(L[Number(arg)]); break;
case "local.set": L[Number(arg)] = pop(); break;
case "drop": pop(); break;
case "i32.add": { const b = pop(), a = pop(); stack.push((a + b) | 0); break; }
case "i32.sub": { const b = pop(), a = pop(); stack.push((a - b) | 0); break; }
case "i32.mul": { const b = pop(), a = pop(); stack.push(Math.imul(a, b)); break; }
default: throw new Error(`Unknown instruction: ${op}`);
}
}
return { result: stack[stack.length - 1], stack };
}