Temporary chapter name for all chapters
context-struct

Look at the signatures we've been accumulating:

fn parseExpression(
    source: [*]const u8,
    pos: *usize,
    names: *[256][]const u8,
    values: *[256]i64,
    count: *usize,
) i64 { ... }

Five parameters. Every parser function has them. Every recursive call passes them all. Every testCase ends with &varNames, &varValues, &varCount. It's silly.

And it's about to get worse. Functions need a function table, a return flag, a return value, and a stack to save/restore the caller's variables. That would add another seven or eight parameters.

So now we make a Context struct. One pointer replaces all of it. This is the first and only struct we're going to use in the whole language -- we put it off until the argument lists genuinely hurt. Now they do.

const Context = struct {
    source: [*]const u8 = undefined,
    pos: usize = 0,

    varNames: [256][]const u8 = undefined,
    varValues: [256]i64 = undefined,
    varCount: usize = 0,

    // more fields coming in the next few problems:
    // function table, return flag, call-save stack.
};

Field access on a pointer auto-dereferences, so ctx.pos works even though ctx is *Context.

Rewrite every parser, executor, and variable function to take ctx: *Context as their only argument. The small text-only helpers (skipSpaces, readNumber, readIdentifier, isSpace, etc.) keep their existing (source, pos) signature -- they don't need the whole context. Call them as skipSpaces(ctx.source, &ctx.pos).

parseFactor:

fn parseFactor(ctx: *Context) i64 {
    if (ctx.source[ctx.pos] == '(') {
        ctx.pos += 1;
        skipSpaces(ctx.source, &ctx.pos);
        const val = parseExpression(ctx);
        if (ctx.source[ctx.pos] == ')') {
            ctx.pos += 1;
            skipSpaces(ctx.source, &ctx.pos);
        }
        return val;
    }
    if (isAlpha(ctx.source[ctx.pos])) {
        const name = readIdentifier(ctx.source, &ctx.pos);
        return lookupVariable(ctx, name);
    }
    return readNumber(ctx.source, &ctx.pos);
}

The rest of the parser chain follows the same shape -- parseTerm(ctx), parseAddSub(ctx), parseExpression(ctx).

lookupVariable and assignVariable now take ctx:

fn lookupVariable(ctx: *Context, name: []const u8) i64 {
    var i: usize = ctx.varCount;
    while (i > 0) {
        i -= 1;
        if (stringsEqual(ctx.varNames[i], name)) {
            return ctx.varValues[i];
        }
    }
    return 0;
}

fn assignVariable(ctx: *Context, name: []const u8, val: i64) void {
    var i: usize = 0;
    while (i < ctx.varCount) {
        if (stringsEqual(ctx.varNames[i], name)) {
            ctx.varValues[i] = val;
            return;
        }
        i += 1;
    }
    ctx.varNames[ctx.varCount] = name;
    ctx.varValues[ctx.varCount] = val;
    ctx.varCount += 1;
}

executeStatement, executeBlock, runProgram, skipBlock all take ctx. (skipBlock only uses ctx.source and ctx.pos.)

eval sets up the source and kicks off runProgram:

fn eval(input: []const u8, ctx: *Context) i64 {
    ctx.source = input.ptr;
    ctx.pos = 0;
    skipSpaces(ctx.source, &ctx.pos);
    return runProgram(ctx);
}

testCase and main:

fn testCase(input: []const u8, expected: i64, ctx: *Context) void {
    const got = eval(input, ctx);
    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 ctx = Context{};
    testCase("var x: i64 = 42; x", 42, &ctx);
    testCase("var x: i64 = 5; x + 1", 6, &ctx);
    testCase("var x: i64 = 5; x = x + 1; x", 6, &ctx);
}

Notice var ctx = Context{}; -- the struct's defaults (undefined for the arrays, 0 for pos and varCount) take care of initialization. Every call site is now (ctx) or (input, ctx). One pointer. The struct will grow silently as we add function-related fields; call sites won't change.