Parsing is reading bytes, splitting tokens, converting types, and verifying output. If the output is predictable, the parser is correct. I built a small OBJ vertex parser in Zig because Zig makes allocation and errors explicit, which forces you to understand what the parser is actually doing.

What the parser does

It reads a .obj file, extracts v vertex lines, parses the three floats, stores them in a dynamic list, and prints them back.

FilePurpose
data/sample.objKnown test input
src/main.zigThe parser
Execution process checkpoint illustration for this section.

Setup

mkdir zig-obj
cd zig-obj
zig init-exe
mkdir -p data
cat > data/sample.obj <<'EOF'
v 0.0 1.0 0.0
v -1.0 0.0 0.0
v 1.0 0.0 0.0
EOF

The parser

const std = @import("std");
const Vertex = struct {
x: f32,
y: f32,
z: f32,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var file = try std.fs.cwd().openFile("data/sample.obj", .{});
defer file.close();
const reader = file.reader();
var buf_reader = std.io.bufferedReader(reader);
var in_stream = buf_reader.reader();
var vertices = std.ArrayList(Vertex).init(allocator);
defer vertices.deinit();
var line_buf: [512]u8 = undefined;
while (try in_stream.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {
if (line.len == 0) continue;
if (line[0] != 'v') continue;
var it = std.mem.tokenize(u8, line, " ");
_ = it.next(); // skip "v"
const x = try std.fmt.parseFloat(f32, it.next().?);
const y = try std.fmt.parseFloat(f32, it.next().?);
const z = try std.fmt.parseFloat(f32, it.next().?);
try vertices.append(.{ .x = x, .y = y, .z = z });
}
const out = std.io.getStdOut().writer();
for (vertices.items) |v| {
try out.print("v {d:.1} {d:.1} {d:.1}\n", .{ v.x, v.y, v.z });
}
}

Run it

zig build run

Expected output:

v 0.0 1.0 0.0
v -1.0 0.0 0.0
v 1.0 0.0 0.0

If the output matches the input exactly, the parser works for this input.

Delivery workflow checkpoint illustration for this section.

Why this is useful

This parser exercises the pieces that matter for low-level parsing: file I/O, buffered reading, tokenization, numeric conversion, dynamic allocation, and formatted output. Every allocation is explicit. Every failure is an error union. Zig does not let you ignore the hard parts, which is why it is a good teacher for parsing.

Common mistakes I hit:

  • Forgetting the try on file open or parse calls.
  • Missing the defer to clean up the allocator.
  • Assuming a line always starts with v and not checking line[0].

Closing

A parser is correct when input, logic, and output are all visible. This example is small, but the same loop applies to larger formats: read, tokenize, parse, store, verify.