Temporary chapter name for all chapters
function-calls

Add function calls to parseFactor. When we see name(, it's a call. Evaluate the arguments, then hand off to callFunction which saves the caller's variables, binds parameters, runs the body, and restores everything.

The save/restore needs a stack. Add to Context:

saveNames: [32][256][]const u8 = undefined,
saveValues: [32][256]i64 = undefined,
saveCounts: [32]usize = undefined,
saveDepth: usize = 0,

Thirty-two deep. Fibonacci won't reach that without a lot of recursion -- but easy to raise.

    testCase("fn add(a: i64, b: i64) i64 { return a + b; } add(3, 5)", 8, &ctx);
    testCase("fn double(x: i64) i64 { return x * 2; } double(21)", 42, &ctx);

Update the name branch of parseFactor to look for ( after the identifier:

    if (isAlpha(ctx.source[ctx.pos])) {
        const name = readIdentifier(ctx.source, &ctx.pos);
        if (ctx.source[ctx.pos] == '(') {
            ctx.pos += 1;
            skipSpaces(ctx.source, &ctx.pos);
            var args: [8]i64 = undefined;
            var argCount: usize = 0;
            while (ctx.source[ctx.pos] != ')' and ctx.source[ctx.pos] != 0) {
                args[argCount] = parseExpression(ctx);
                argCount += 1;
                if (ctx.source[ctx.pos] == ',') {
                    ctx.pos += 1;
                    skipSpaces(ctx.source, &ctx.pos);
                }
            }
            if (ctx.source[ctx.pos] == ')') {
                ctx.pos += 1;
                skipSpaces(ctx.source, &ctx.pos);
            }
            return callFunction(ctx, name, &args, argCount);
        }
        return lookupVariable(ctx, name);
    }

callFunction:

fn callFunction(ctx: *Context, name: []const u8, args: *const [8]i64, argCount: usize) i64 {
    _ = argCount;
    // Find the function
    var fi: usize = 0;
    while (fi < ctx.fnCount) {
        if (stringsEqual(ctx.fnNames[fi], name)) break;
        fi += 1;
    }
    if (fi >= ctx.fnCount) return 0;

    // Save caller state
    const d = ctx.saveDepth;
    ctx.saveCounts[d] = ctx.varCount;
    const savedPos = ctx.pos;
    var i: usize = 0;
    while (i < ctx.varCount) {
        ctx.saveNames[d][i] = ctx.varNames[i];
        ctx.saveValues[d][i] = ctx.varValues[i];
        i += 1;
    }
    ctx.saveDepth += 1;

    // Bind parameters
    ctx.varCount = 0;
    var p: usize = 0;
    while (p < ctx.fnParamCounts[fi]) {
        assignVariable(ctx, ctx.fnParams[fi][p], args[p]);
        p += 1;
    }

    // Execute body
    ctx.pos = ctx.fnStarts[fi];
    ctx.returnFlag = false;
    const result = executeBlock(ctx);

    // Restore caller state
    ctx.saveDepth -= 1;
    ctx.pos = savedPos;
    ctx.varCount = ctx.saveCounts[d];
    var j: usize = 0;
    while (j < ctx.varCount) {
        ctx.varNames[j] = ctx.saveNames[d][j];
        ctx.varValues[j] = ctx.saveValues[d][j];
        j += 1;
    }
    ctx.returnFlag = false;
    return result;
}

This is the biggest function in the interpreter. Walk through it with a debugger on the first call to double(21). The caller's variables (none yet, first test) get snapshotted into ctx.saveNames[0], ctx.varCount resets to 0, the parameter x is bound to 21, the body runs, return x * 2; triggers ctx.returnFlag, executeBlock hands back 42, and then we restore the caller's state from the snapshot. Phew.