Add the heap to the interpreter. A global byte array, 1MB:
var heap: [1 << 20]u8 = [_]u8{0} ** (1 << 20);
Add load8 and store8 as builtin functions. When the interpreter sees load8(addr) or store8(addr, val) in factor() (where function calls are handled), it executes them directly instead of looking for a user-defined function.
In the interpreter's call path (inside factor(), where we handle name(), add builtin checks before searching user-defined functions:
if (streq(name, "load8")) {
pos += 1; skip();
const addr: i64 = expression();
if (cur() == ')') { pos += 1; skip(); }
return @as(i64, heap[@intCast(addr)]);
}
if (streq(name, "store8")) {
pos += 1; skip();
const addr: i64 = expression();
if (cur() == ',') { pos += 1; skip(); }
const val: i64 = expression();
if (cur() == ')') { pos += 1; skip(); }
heap[@intCast(addr)] = @intCast(val);
return 0;
}
if (streq(name, "load64")) {
pos += 1; skip();
const addr: i64 = expression();
if (cur() == ')') { pos += 1; skip(); }
const a: usize = @intCast(addr);
var val: i64 = 0;
for (0..8) |i| { val = val | (@as(i64, heap[a + i]) << @intCast(i * 8)); }
return val;
}
if (streq(name, "store64")) {
pos += 1; skip();
const addr: i64 = expression();
if (cur() == ',') { pos += 1; skip(); }
const val: i64 = expression();
if (cur() == ')') { pos += 1; skip(); }
const a: usize = @intCast(addr);
const v: u64 = @bitCast(val);
for (0..8) |i| { heap[a + i] = @intCast((v >> @intCast(i * 8)) & 0xff); }
return 0;
}
Test:
check("store8(1000, 65); load8(1000)", 65);
check("store64(2000, 123456); load64(2000)", 123456);
check("store64(2000, 0 - 1); load64(2000)", -1);
load8/store8 handle single bytes. load64/store64 handle full 64-bit integers -- 8 bytes, little-endian, just like WebAssembly's i64.load/i64.store. We need both: bytes for character data, 64-bit words for arrays of integers. The self-hosted compiler's function table, parameter lists, and string table all store i64 values.