We're not going to use Zig's standard library. No @import("std"), because self-hosting is about eating our own code. When we make our job easier, we make our job harder.
But then, how do we print? We grab one function from libc -- putchar, which writes a single byte -- and build everything else on it.
Replace your minizig.zig with this:
extern "c" fn putchar(c: c_int) c_int;
fn printChar(c: u8) void {
_ = putchar(@intCast(c));
}
fn printString(s: []const u8) void {
var i: usize = 0;
while (i < s.len) {
printChar(s[i]);
i += 1;
}
}
pub fn main() void {
printString("hello, world\n");
}
Build with -lc so putchar resolves against libc:
zig build-exe minizig.zig -lc
Same hello, world output, no std.
We'll need three numeric types through the book:
- u8: unsigned 8-bit integer. Range 0 to 255. One byte. Mostly for individual characters.
- i64: signed 64-bit integer. Range -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Our default for any number.
- usize: an unsigned integer big enough to hold any array index or length on your machine (usually 64 bits). Use it for positions into a string, array lengths, counts. If the distinction from u64 ever matters, it doesn't in this book.
Your turn. Write printNumber(n: i64) void. It should print 42 for 42, 0 for 0, -123 for -123. No recursion.
Hint: collect digits low-to-high into a small buffer, then print the buffer in reverse. Special-case zero (otherwise the loop never runs). Handle negatives by printing '-' then flipping the sign.
@mod(value, 10) gives the last digit as an i64. @divTrunc(value, 10) drops the last digit. @intCast converts between integer types (e.g. i64 to u8).
Test it:
pub fn main() void {
printNumber(42); printChar('\n');
printNumber(0); printChar('\n');
printNumber(-123); printChar('\n');
printNumber(999999);printChar('\n');
}
Use what you set up in the last problem. Put a breakpoint on the first printNumber(42) call. Press F5 -- the program freezes there. Press F11 to step into printNumber. You're now inside the function, with n showing 42. Press F10 to step over each line; hover over value, i, buf to watch them change. When you hit return, F10 pops you back out to main. Try printNumber(1234) and watch the buffer fill in reverse order, one digit per F10 press. That's the debugger doing what print can't.
fn printNumber(n: i64) void {
var value: i64 = n;
if (value < 0) {
printChar('-');
value = -value;
}
if (value == 0) {
printChar('0');
return;
}
var buf: [20]u8 = undefined;
var i: usize = 0;
while (value > 0) {
const d: i64 = @mod(value, 10);
buf[i] = @intCast('0' + d);
i += 1;
value = @divTrunc(value, 10);
}
while (i > 0) {
i -= 1;
printChar(buf[i]);
}
}
Buffer is [20]u8 because i64 maxes out at 19 digits; plus room. undefined is fine -- we only read slots we wrote.