Add load8 and store8 to the VM. When vm_exec_line sees these instructions, operate on a VM-side memory array:
var vm_memory: [1 << 20]u8 = [_]u8{0} ** (1 << 20);
if (streq(line, "i32.load8_u")) {
const addr: i64 = vm_pop();
vm_push(@as(i64, vm_memory[@intCast(addr)]));
return;
}
if (streq(line, "i32.store8")) {
const val: i64 = vm_pop();
const addr: i64 = vm_pop();
vm_memory[@intCast(addr)] = @intCast(val);
return;
}
if (streq(line, "i64.load")) {
const addr: usize = @intCast(vm_pop());
var val: i64 = 0;
for (0..8) |i| { val = val | (@as(i64, vm_memory[addr + i]) << @intCast(i * 8)); }
vm_push(val);
return;
}
if (streq(line, "i64.store")) {
const val: i64 = vm_pop();
const addr: usize = @intCast(vm_pop());
const v: u64 = @bitCast(val);
for (0..8) |i| { vm_memory[addr + i] = @intCast((v >> @intCast(i * 8)) & 0xff); }
return;
}
if (streq(line, "drop")) {
_ = vm_pop();
return;
}
if (streq(line, "i32.wrap_i64")) {
// truncate to 32 bits -- for our purposes, a no-op on small values
return;
}
if (streq(line, "i64.extend_i32_u")) {
// extend to 64 bits -- also a no-op for us
return;
}
Test agreement:
check_both("store8(1000, 65); load8(1000)", 65);
check_both("store8(100, 72); store8(101, 105); load8(100) * 256 + load8(101)", 72 * 256 + 105);
Both engines agree. Memory works in the interpreter, the compiler, and the VM.
### Strings
Here's the plan. A string literal like "hello" does two things:
1. Stores the bytes h, e, l, l, o somewhere in memory
2. Evaluates to two values: the address where the bytes start, and the length
We'll use a convention: string data lives at high addresses (starting at 60000), and a string expression evaluates to (addr * 65536 + len) -- the address and length packed into a single i64. We can extract them: addr = val / 65536, len = val % 65536.
(In a real language you'd use two values or a struct. Packing them into one i64 is a hack, but it keeps our language simple -- we don't need tuples or multiple return values.)