Temporary chapter name for all chapters
factor-handles-names

Now hook variables into the parser. Three sweeping changes:

1. parseFactor learns to read names. When the current byte is a letter, read an identifier and look up its value.
2. All parser functions gain three parameters (names, values, count pointers). They need to pass them down to recursive calls and to lookupVariable. This is tedious but simple plumbing -- we'll clean it up with a struct later, once it gets unbearable.
3. New functions executeStatement and runProgram. A statement is a var/const declaration, an assignment, or a bare expression. runProgram loops, executing statements until end-of-input or a }.

eval becomes a thin entry point: set up pos, skip leading spaces, call runProgram. testCase and main grow to thread the variable pointers through.

The var handler parses var name: i64 = expr; -- it reads the name, consumes : i64 (parsing the type but not using it yet), then evaluates the initializer. We throw the type away for now. When we compile to WAT later, we'll need it to emit (local $x i64); the groundwork is being laid quietly.

    testCase("var x: i64 = 42; x", 42, &varNames, &varValues, &varCount);
    testCase("var x: i64 = 5; x + 1", 6, &varNames, &varValues, &varCount);
    testCase("var x: i64 = 5; x = x + 1; x", 6, &varNames, &varValues, &varCount);
    testCase("var a: i64 = 10; var b: i64 = 20; a + b", 30, &varNames, &varValues, &varCount);

parseFactor with the name branch:

fn parseFactor(
    source: [*]const u8,
    pos: *usize,
    names: *[256][]const u8,
    values: *[256]i64,
    count: *usize,
) i64 {
    if (source[pos.*] == '(') {
        pos.* += 1;
        skipSpaces(source, pos);
        const val: i64 = parseExpression(source, pos, names, values, count);
        if (source[pos.*] == ')') {
            pos.* += 1;
            skipSpaces(source, pos);
        }
        return val;
    }
    if (isAlpha(source[pos.*])) {
        const name = readIdentifier(source, pos);
        return lookupVariable(names, values, count.*, name);
    }
    return readNumber(source, pos);
}

parseTerm, parseAddSub, parseExpression gain the same three parameters and pass them through. Nothing else about them changes.

executeStatement -- dispatches on what kind of statement we're looking at:

fn executeStatement(
    source: [*]const u8,
    pos: *usize,
    names: *[256][]const u8,
    values: *[256]i64,
    count: *usize,
) i64 {
    if (isAlpha(source[pos.*])) {
        const savedPos = pos.*;
        const firstWord = readIdentifier(source, pos);

        // var/const declaration: var name: type = expr;
        if (stringsEqual(firstWord, "var") or stringsEqual(firstWord, "const")) {
            const varName = readIdentifier(source, pos);
            if (source[pos.*] == ':') {
                pos.* += 1;
                skipSpaces(source, pos);
                _ = readIdentifier(source, pos);   // parse the type, ignore it
            }
            if (source[pos.*] == '=') {
                pos.* += 1;
                skipSpaces(source, pos);
            }
            const initValue = parseExpression(source, pos, names, values, count);
            if (source[pos.*] == ';') {
                pos.* += 1;
                skipSpaces(source, pos);
            }
            assignVariable(names, values, count, varName, initValue);
            return initValue;
        }

        // assignment: name = expr; (not == which is a comparison)
        if (source[pos.*] == '=' and source[pos.* + 1] != '=') {
            pos.* += 1;
            skipSpaces(source, pos);
            const newValue = parseExpression(source, pos, names, values, count);
            if (source[pos.*] == ';') {
                pos.* += 1;
                skipSpaces(source, pos);
            }
            assignVariable(names, values, count, firstWord, newValue);
            return newValue;
        }

        // not a keyword or assignment: rewind and treat as expression
        pos.* = savedPos;
    }

    const result = parseExpression(source, pos, names, values, count);
    if (source[pos.*] == ';') {
        pos.* += 1;
        skipSpaces(source, pos);
    }
    return result;
}

runProgram -- loops until end-of-input or }:

fn runProgram(
    source: [*]const u8,
    pos: *usize,
    names: *[256][]const u8,
    values: *[256]i64,
    count: *usize,
) i64 {
    var lastValue: i64 = 0;
    while (source[pos.*] != 0 and source[pos.*] != '}') {
        lastValue = executeStatement(source, pos, names, values, count);
    }
    return lastValue;
}

The updated eval, testCase, and main:

fn eval(
    input: []const u8,
    names: *[256][]const u8,
    values: *[256]i64,
    count: *usize,
) i64 {
    const source: [*]const u8 = input.ptr;
    var pos: usize = 0;
    skipSpaces(source, &pos);
    return runProgram(source, &pos, names, values, count);
}

fn testCase(
    input: []const u8,
    expected: i64,
    names: *[256][]const u8,
    values: *[256]i64,
    count: *usize,
) void {
    const got: i64 = eval(input, names, values, count);
    if (got == expected) {
        print("{s} = {d} ok\n", .{ input, got });
    } else {
        print("{s} = {d} FAIL want {d}\n", .{ input, got, expected });
    }
}

pub fn main() void {
    var varNames: [256][]const u8 = undefined;
    var varValues: [256]i64 = undefined;
    var varCount: usize = 0;

    testCase("var x: i64 = 42; x", 42, &varNames, &varValues, &varCount);
    testCase("var x: i64 = 5; x + 1", 6, &varNames, &varValues, &varCount);
    testCase("var x: i64 = 5; x = x + 1; x", 6, &varNames, &varValues, &varCount);
    testCase("var a: i64 = 10; var b: i64 = 20; a + b", 30, &varNames, &varValues, &varCount);
}

The &varNames, &varValues, &varCount tail on every call is painful. We'll fix it with a struct soon.