Let's take a little detour to recap Zig's syntax around arrays and pointers:
- [25]u8 is an array. It is a value and it lives on the stack, just like any value.
- *u8 is a pointer to a u8.
- [*]u8 is a pointer to a u8 that is part of an array (even though the syntax is weird, this is the most normal one).
- []u8 is a combination of a [*]u8 and a size: it is a slice, a view into an array, a fat pointer.
- [:0]u8 is a slice that ends with a zero byte (length AND sentinel).
- [*:0]u8 is like [*]u8 but the array it points into ends with a zero byte. Walk until you see the zero.
- & in front of something gives you a pointer to it (or something that is some sort of pointer such as a slice).
- .* after a pointer is the opposite: it dereferences, which means "follow this pointer".
When you write "hello", Zig puts the constant bytes h e l l o 0 in the "data section of your executable" and hands you a [:0]const u8 so you can access it.
If you are feeling overwhelmed, don't worry too much. You will learn to love Zig's explicitness around pointers; it is one of its best features.
Back to WAT. We now want to generate it instead of writing it by hand. Two helpers do the heavy lifting.
The helpers below take &wat_buffer and &wat_length so they can write into our buffer and bump the length. Inside, position.* reads or writes the value the pointer points at -- the .* "unwraps" the pointer.
fn appendChar(
buffer: [*]u8,
position: *usize,
c: u8,
) void {
buffer[position.*] = c;
position.* += 1;
}
fn appendStringLiteral(
buffer: [*]u8,
position: *usize,
s: [*:0]const u8,
) void {
var i: usize = 0;
while (s[i] != 0) {
appendChar(buffer, position, s[i]);
i += 1;
}
}
appendStringLiteral walks s until it sees the zero -- that's why s is [*:0]const u8. Every Zig string literal has that zero at the end, including the multi-line \\ ones.
We keep both paths in main and pick between them with write_wat_manually. When true, paste the WAT in by hand; when false, the compiler emits it. Either way the buffer ends up the same. Same vm result.
Fill in the else branch.
pub fn main() void {
const input: [:0]const u8 = "3+4";
const a: i64 = @as(i64, input[0]) - '0';
const b: i64 = @as(i64, input[2]) - '0';
const result: i64 = a + b;
printString("interpreter result for ");
printString(input);
printString(" : ");
printNumber(result);
printChar('\n');
var wat_buffer: [256]u8 = undefined;
var wat_length: usize = 0;
const write_wat_manually: bool = false;
if (write_wat_manually) {
appendStringLiteral(&wat_buffer, &wat_length,
\\i64.const 3
\\i64.const 4
\\i64.add
);
} else {
// YOU: emit "i64.const X\n" for input[0],
// "i64.const Y\n" for input[2], "i64.add\n".
}
var stack: [16]i64 = undefined;
var stack_pointer: usize = 0;
var wat_pos: usize = 0;
while (wat_pos < wat_length) {
var wat_line_end: usize = wat_pos;
while (wat_line_end < wat_length and wat_buffer[wat_line_end] != '\n') {
wat_line_end += 1;
}
const line: []const u8 = wat_buffer[wat_pos..wat_line_end];
if (stringStartsWith(line, "i64.const ")) {
stack[stack_pointer] = @as(i64, line[line.len - 1]) - '0';
stack_pointer += 1;
} else if (stringsEqual(line, "i64.add")) {
const y: i64 = stack[stack_pointer - 1];
const x: i64 = stack[stack_pointer - 2];
stack_pointer -= 2;
stack[stack_pointer] = x + y;
stack_pointer += 1;
} else {
printString("error: unknown instruction\n");
return;
}
wat_pos = wat_line_end + 1;
}
printString("vm result: ");
printNumber(stack[0]);
printChar('\n');
}
appendStringLiteral(&wat_buffer, &wat_length, "i64.const ");
appendChar(&wat_buffer, &wat_length, input[0]);
appendChar(&wat_buffer, &wat_length, '\n');
appendStringLiteral(&wat_buffer, &wat_length, "i64.const ");
appendChar(&wat_buffer, &wat_length, input[2]);
appendChar(&wat_buffer, &wat_length, '\n');
appendStringLiteral(&wat_buffer, &wat_length, "i64.add\n");
We don't bother converting the digit char to a number and back -- input[0] is already a valid WAT digit. Append it as a byte.
Flip write_wat_manually to true and back. Same 7 either way.