Temporary chapter name for all chapters
is-letter-read-name

Variable names. A name starts with a letter (or _), and can contain letters, digits, and underscores after.

Write two classifiers and an identifier reader.

fn isAlpha(c: u8) bool {
    return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '_';
}

fn isDigit(c: u8) bool {
    return c >= '0' and c <= '9';
}

Why split isAlpha and isDigit? A name must start with a letter, but any byte after can be a digit too. x1 is legal, 1x is not. Two small predicates keep both rules crisp.

While you're here: you can now simplify readNumber to call isDigit instead of its inline range check.

Write readIdentifier: reads a name, returns a slice into the source.

fn readIdentifier(source: [*]const u8, pos: *usize) []const u8 {
    const start = pos.*;
    while (isAlpha(source[pos.*]) or isDigit(source[pos.*])) {
        pos.* += 1;
    }
    const name = source[start..pos.*];
    skipSpaces(source, pos);
    return name;
}

source[start..pos.*] is a slice -- a view into the string. No copying, no allocation. Slices are one of Zig's best features, and we'll use them constantly.

The cleaned-up readNumber:

fn readNumber(source: [*]const u8, pos: *usize) i64 {
    var value: i64 = 0;
    while (isDigit(source[pos.*])) {
        value = value * 10 + digitValue(source[pos.*]);
        pos.* += 1;
    }
    skipSpaces(source, pos);
    return value;
}