Phase 5: scripting language

TaskUpdate · Jul 17, 19:20:44 · toolu_019r5oLV9m8PAGK2WVyxcYjL
{
 "taskId": "5",
 "status": "completed"
}
result
Updated task #5 status
TaskUpdate · Jul 17, 19:20:44 · toolu_01R1M5SViu3q3DnNxXMGBJN5
{
 "taskId": "6",
 "status": "in_progress"
}
result
Updated task #6 status
agent · Jul 17, 19:22:58
Now the scripting language: shared lexer, then the script-specific parser/AST/evaluator.
Bash · Jul 17, 19:22:58 · toolu_017ri2HPbfAyaT5iLng3m1Ny
Create lang and lang/script directories
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script
result
(Bash completed with no output)
Write · Jul 17, 19:23:06 · toolu_013bVn2HPm1vv9QCbSCRjgAB
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/lexer.h
55 lines
#pragma once

/* Shared tokenizer used by both the scripting language (kernel/lang/script)
 * and the tiny compiler (kernel/lang/compiler). Keywords are not baked in
 * here - callers recognize keywords by comparing TOK_IDENT text, since the
 * two languages don't share a keyword set. */

#include <stdint.h>

typedef enum {
    TOK_EOF,
    TOK_IDENT,
    TOK_NUMBER,
    TOK_STRING,
    TOK_LPAREN,
    TOK_RPAREN,
    TOK_LBRACE,
    TOK_RBRACE,
    TOK_COMMA,
    TOK_SEMI,
    TOK_PLUS,
    TOK_MINUS,
    TOK_STAR,
    TOK_SLASH,
    TOK_PERCENT,
    TOK_ASSIGN,
    TOK_EQ,
    TOK_NEQ,
    TOK_LT,
    TOK_LE,
    TOK_GT,
    TOK_GE,
    TOK_NOT,
    TOK_AND,
    TOK_OR,
    TOK_ERROR,
} token_type_t;

#define TOKEN_TEXT_MAX 32

typedef struct {
    token_type_t type;
    char text[TOKEN_TEXT_MAX]; /* TOK_IDENT */
    int32_t number;             /* TOK_NUMBER */
    char *string;                /* TOK_STRING - kmalloc'd, owned by the AST node it ends up in */
} token_t;

typedef struct {
    const char *src;
    int pos;
} lexer_t;

void lexer_init(lexer_t *lx, const char *src);
token_t lexer_next(lexer_t *lx);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/lexer.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:23:19 · toolu_017LMXaAJEtUYKXFEoheVPVw
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/lexer.c
139 lines
#include "lexer.h"

#include "../memory.h"
#include "../string.h"

static int is_alpha(char c) {
    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}

static int is_digit(char c) {
    return c >= '0' && c <= '9';
}

static int is_alnum(char c) {
    return is_alpha(c) || is_digit(c);
}

static void skip_ws_and_comments(lexer_t *lx) {
    for (;;) {
        char c = lx->src[lx->pos];
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
            lx->pos++;
        } else if (c == '#') {
            while (lx->src[lx->pos] && lx->src[lx->pos] != '\n') {
                lx->pos++;
            }
        } else {
            break;
        }
    }
}

void lexer_init(lexer_t *lx, const char *src) {
    lx->src = src;
    lx->pos = 0;
}

token_t lexer_next(lexer_t *lx) {
    token_t tok;
    tok.string = NULL;
    tok.number = 0;
    tok.text[0] = '\0';

    skip_ws_and_comments(lx);
    char c = lx->src[lx->pos];

    if (c == '\0') {
        tok.type = TOK_EOF;
        return tok;
    }

    if (is_alpha(c)) {
        int start = lx->pos;
        while (is_alnum(lx->src[lx->pos])) {
            lx->pos++;
        }
        int len = lx->pos - start;
        if (len >= TOKEN_TEXT_MAX) {
            len = TOKEN_TEXT_MAX - 1;
        }
        memcpy(tok.text, lx->src + start, (size_t)len);
        tok.text[len] = '\0';
        tok.type = TOK_IDENT;
        return tok;
    }

    if (is_digit(c)) {
        int32_t val = 0;
        while (is_digit(lx->src[lx->pos])) {
            val = val * 10 + (lx->src[lx->pos] - '0');
            lx->pos++;
        }
        tok.number = val;
        tok.type = TOK_NUMBER;
        return tok;
    }

    if (c == '"') {
        lx->pos++;
        int start = lx->pos;
        while (lx->src[lx->pos] && lx->src[lx->pos] != '"') {
            lx->pos++;
        }
        int len = lx->pos - start;
        char *s = kmalloc((size_t)len + 1);
        memcpy(s, lx->src + start, (size_t)len);
        s[len] = '\0';
        if (lx->src[lx->pos] == '"') {
            lx->pos++;
        }
        tok.string = s;
        tok.type = TOK_STRING;
        return tok;
    }

    lx->pos++;
    switch (c) {
        case '(': tok.type = TOK_LPAREN; break;
        case ')': tok.type = TOK_RPAREN; break;
        case '{': tok.type = TOK_LBRACE; break;
        case '}': tok.type = TOK_RBRACE; break;
        case ',': tok.type = TOK_COMMA; break;
        case ';': tok.type = TOK_SEMI; break;
        case '+': tok.type = TOK_PLUS; break;
        case '-': tok.type = TOK_MINUS; break;
        case '*': tok.type = TOK_STAR; break;
        case '/': tok.type = TOK_SLASH; break;
        case '%': tok.type = TOK_PERCENT; break;
        case '=':
            if (lx->src[lx->pos] == '=') { lx->pos++; tok.type = TOK_EQ; }
            else { tok.type = TOK_ASSIGN; }
            break;
        case '!':
            if (lx->src[lx->pos] == '=') { lx->pos++; tok.type = TOK_NEQ; }
            else { tok.type = TOK_NOT; }
            break;
        case '<':
            if (lx->src[lx->pos] == '=') { lx->pos++; tok.type = TOK_LE; }
            else { tok.type = TOK_LT; }
            break;
        case '>':
            if (lx->src[lx->pos] == '=') { lx->pos++; tok.type = TOK_GE; }
            else { tok.type = TOK_GT; }
            break;
        case '&':
            if (lx->src[lx->pos] == '&') { lx->pos++; tok.type = TOK_AND; }
            else { tok.type = TOK_ERROR; }
            break;
        case '|':
            if (lx->src[lx->pos] == '|') { lx->pos++; tok.type = TOK_OR; }
            else { tok.type = TOK_ERROR; }
            break;
        default:
            tok.type = TOK_ERROR;
            break;
    }
    return tok;
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/lexer.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:23:21
Now the AST:
Write · Jul 17, 19:23:25 · toolu_01Fx4HBtLp5pqJyjoUQNXZtp
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/ast.h
42 lines
#pragma once

#include "../lexer.h"

typedef enum {
    AST_NUM,
    AST_STR,
    AST_VAR,
    AST_ASSIGN,
    AST_BINOP,
    AST_UNOP,
    AST_CALL,
    AST_IF,
    AST_WHILE,
    AST_FUNC_DEF,
    AST_RETURN,
    AST_BLOCK,
    AST_EXPR_STMT,
} ast_kind_t;

typedef struct ast_node ast_node_t;

/* One generic node shape for the whole tree, in the tradition of tiny
 * interpreters: `a`/`b`/`c` are kind-specific children, `list` is a
 * singly-linked chain (block statements, call args, function params),
 * and `next` links siblings within such a chain. See eval.c/parser.c for
 * which fields each AST_* kind actually uses. */
struct ast_node {
    ast_kind_t kind;
    token_type_t op; /* AST_BINOP / AST_UNOP */

    int32_t num;  /* AST_NUM */
    char *str;     /* AST_STR - shares the token's kmalloc'd buffer */
    char name[TOKEN_TEXT_MAX]; /* AST_VAR / AST_ASSIGN / AST_CALL / AST_FUNC_DEF */

    ast_node_t *a, *b, *c;
    ast_node_t *list;
    ast_node_t *next;
};

ast_node_t *ast_new(ast_kind_t kind);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/ast.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:23:30 · toolu_01FTjJEmL1mZN6TXgxmX1o1d
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/ast.c
#include "ast.h"

#include "../../memory.h"
#include "../../string.h"

/* Nodes are allocated from the kernel heap and never freed - a script's
 * whole AST lives for the duration of one `run`. Fine for tiny scripts;
 * the heap resets on reboot. */
ast_node_t *ast_new(ast_kind_t kind) {
    ast_node_t *n = kmalloc(sizeof(ast_node_t));
    memset(n, 0, sizeof(*n));
    n->kind = kind;
    return n;
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/ast.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:23:32
Now the parser:
Write · Jul 17, 19:23:34 · toolu_01W9wHfYbncxU4nfYegg9Cs5
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/parser.h
#pragma once

#include "../lexer.h"
#include "ast.h"

typedef struct {
    lexer_t lexer;
    token_t cur;
    int error;
    char error_msg[64];
} parser_t;

void parser_init(parser_t *p, const char *src);

/* Parses the whole source as a top-level AST_BLOCK of statements. Check
 * p->error afterwards before evaluating the result. */
ast_node_t *parse_program(parser_t *p);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/parser.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:23:59 · toolu_012WmSovpkL2eghZY6E4XkZJ
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/parser.c
310 lines
#include "parser.h"

#include "../../string.h"

static void copy_name(char *dst, const char *src) {
    int i = 0;
    while (src[i] && i < TOKEN_TEXT_MAX - 1) {
        dst[i] = src[i];
        i++;
    }
    dst[i] = '\0';
}

static void advance(parser_t *p) {
    p->cur = lexer_next(&p->lexer);
}

void parser_init(parser_t *p, const char *src) {
    lexer_init(&p->lexer, src);
    p->error = 0;
    p->error_msg[0] = '\0';
    advance(p);
}

static void parse_error(parser_t *p, const char *msg) {
    if (!p->error) {
        p->error = 1;
        copy_name(p->error_msg, msg); /* error_msg is 64 bytes, plenty for our short messages */
    }
}

static int check(parser_t *p, token_type_t t) {
    return p->cur.type == t;
}

static int match(parser_t *p, token_type_t t) {
    if (check(p, t)) {
        advance(p);
        return 1;
    }
    return 0;
}

static void expect(parser_t *p, token_type_t t) {
    if (!match(p, t)) {
        parse_error(p, "unexpected token");
    }
}

static int is_kw(parser_t *p, const char *kw) {
    return check(p, TOK_IDENT) && strcmp(p->cur.text, kw) == 0;
}

static ast_node_t *parse_expression(parser_t *p);
static ast_node_t *parse_statement(parser_t *p);

static ast_node_t *parse_block(parser_t *p) {
    expect(p, TOK_LBRACE);
    ast_node_t *blk = ast_new(AST_BLOCK);
    ast_node_t *tail = NULL;
    while (!check(p, TOK_RBRACE) && !check(p, TOK_EOF) && !p->error) {
        ast_node_t *s = parse_statement(p);
        if (!blk->list) { blk->list = s; } else { tail->next = s; }
        tail = s;
    }
    expect(p, TOK_RBRACE);
    return blk;
}

static ast_node_t *parse_primary(parser_t *p) {
    if (check(p, TOK_NUMBER)) {
        ast_node_t *n = ast_new(AST_NUM);
        n->num = p->cur.number;
        advance(p);
        return n;
    }
    if (check(p, TOK_STRING)) {
        ast_node_t *n = ast_new(AST_STR);
        n->str = p->cur.string;
        advance(p);
        return n;
    }
    if (check(p, TOK_IDENT)) {
        char name[TOKEN_TEXT_MAX];
        copy_name(name, p->cur.text);
        advance(p);
        if (check(p, TOK_LPAREN)) {
            advance(p);
            ast_node_t *n = ast_new(AST_CALL);
            copy_name(n->name, name);
            ast_node_t *tail = NULL;
            if (!check(p, TOK_RPAREN)) {
                for (;;) {
                    ast_node_t *arg = parse_expression(p);
                    if (!n->list) { n->list = arg; } else { tail->next = arg; }
                    tail = arg;
                    if (!match(p, TOK_COMMA)) {
                        break;
                    }
                }
            }
            expect(p, TOK_RPAREN);
            return n;
        }
        ast_node_t *n = ast_new(AST_VAR);
        copy_name(n->name, name);
        return n;
    }
    if (match(p, TOK_LPAREN)) {
        ast_node_t *n = parse_expression(p);
        expect(p, TOK_RPAREN);
        return n;
    }
    parse_error(p, "expected expression");
    advance(p); /* avoid getting stuck */
    return ast_new(AST_NUM);
}

static ast_node_t *parse_unary(parser_t *p) {
    if (check(p, TOK_MINUS) || check(p, TOK_NOT)) {
        token_type_t op = p->cur.type;
        advance(p);
        ast_node_t *n = ast_new(AST_UNOP);
        n->op = op;
        n->a = parse_unary(p);
        return n;
    }
    return parse_primary(p);
}

static ast_node_t *parse_binop_level(parser_t *p, ast_node_t *(*next)(parser_t *),
                                      const token_type_t *ops, int nops) {
    ast_node_t *left = next(p);
    for (;;) {
        int matched = 0;
        for (int i = 0; i < nops; i++) {
            if (check(p, ops[i])) {
                token_type_t op = p->cur.type;
                advance(p);
                ast_node_t *right = next(p);
                ast_node_t *n = ast_new(AST_BINOP);
                n->op = op;
                n->a = left;
                n->b = right;
                left = n;
                matched = 1;
                break;
            }
        }
        if (!matched) {
            return left;
        }
    }
}

static ast_node_t *parse_multiplicative(parser_t *p) {
    static const token_type_t ops[] = { TOK_STAR, TOK_SLASH, TOK_PERCENT };
    return parse_binop_level(p, parse_unary, ops, 3);
}

static ast_node_t *parse_additive(parser_t *p) {
    static const token_type_t ops[] = { TOK_PLUS, TOK_MINUS };
    return parse_binop_level(p, parse_multiplicative, ops, 2);
}

static ast_node_t *parse_relational(parser_t *p) {
    static const token_type_t ops[] = { TOK_LT, TOK_LE, TOK_GT, TOK_GE };
    return parse_binop_level(p, parse_additive, ops, 4);
}

static ast_node_t *parse_equality(parser_t *p) {
    static const token_type_t ops[] = { TOK_EQ, TOK_NEQ };
    return parse_binop_level(p, parse_relational, ops, 2);
}

static ast_node_t *parse_and(parser_t *p) {
    static const token_type_t ops[] = { TOK_AND };
    return parse_binop_level(p, parse_equality, ops, 1);
}

static ast_node_t *parse_or(parser_t *p) {
    static const token_type_t ops[] = { TOK_OR };
    return parse_binop_level(p, parse_and, ops, 1);
}

static ast_node_t *parse_assignment(parser_t *p) {
    if (check(p, TOK_IDENT)) {
        parser_t save = *p; /* plain-old-data struct, safe to snapshot/restore */
        char name[TOKEN_TEXT_MAX];
        copy_name(name, p->cur.text);
        advance(p);
        if (check(p, TOK_ASSIGN)) {
            advance(p);
            ast_node_t *value = parse_assignment(p); /* right-associative */
            ast_node_t *n = ast_new(AST_ASSIGN);
            copy_name(n->name, name);
            n->a = value;
            return n;
        }
        *p = save; /* not an assignment - rewind and parse as a normal expression */
    }
    return parse_or(p);
}

static ast_node_t *parse_expression(parser_t *p) {
    return parse_assignment(p);
}

static ast_node_t *parse_statement(parser_t *p) {
    if (check(p, TOK_LBRACE)) {
        return parse_block(p);
    }

    if (is_kw(p, "if")) {
        advance(p);
        expect(p, TOK_LPAREN);
        ast_node_t *cond = parse_expression(p);
        expect(p, TOK_RPAREN);
        ast_node_t *then_blk = parse_block(p);
        ast_node_t *else_blk = NULL;
        if (is_kw(p, "else")) {
            advance(p);
            else_blk = is_kw(p, "if") ? parse_statement(p) : parse_block(p);
        }
        ast_node_t *n = ast_new(AST_IF);
        n->a = cond;
        n->b = then_blk;
        n->c = else_blk;
        return n;
    }

    if (is_kw(p, "while")) {
        advance(p);
        expect(p, TOK_LPAREN);
        ast_node_t *cond = parse_expression(p);
        expect(p, TOK_RPAREN);
        ast_node_t *body = parse_block(p);
        ast_node_t *n = ast_new(AST_WHILE);
        n->a = cond;
        n->b = body;
        return n;
    }

    if (is_kw(p, "func")) {
        advance(p);
        char name[TOKEN_TEXT_MAX];
        if (check(p, TOK_IDENT)) {
            copy_name(name, p->cur.text);
            advance(p);
        } else {
            parse_error(p, "expected function name");
            name[0] = '\0';
        }
        expect(p, TOK_LPAREN);
        ast_node_t *params = NULL;
        ast_node_t *ptail = NULL;
        if (!check(p, TOK_RPAREN)) {
            for (;;) {
                if (check(p, TOK_IDENT)) {
                    ast_node_t *pn = ast_new(AST_VAR);
                    copy_name(pn->name, p->cur.text);
                    advance(p);
                    if (!params) { params = pn; } else { ptail->next = pn; }
                    ptail = pn;
                } else {
                    parse_error(p, "expected parameter name");
                    break;
                }
                if (!match(p, TOK_COMMA)) {
                    break;
                }
            }
        }
        expect(p, TOK_RPAREN);
        ast_node_t *body = parse_block(p);
        ast_node_t *n = ast_new(AST_FUNC_DEF);
        copy_name(n->name, name);
        n->list = params;
        n->b = body;
        return n;
    }

    if (is_kw(p, "return")) {
        advance(p);
        ast_node_t *n = ast_new(AST_RETURN);
        if (!check(p, TOK_SEMI)) {
            n->a = parse_expression(p);
        }
        expect(p, TOK_SEMI);
        return n;
    }

    ast_node_t *e = parse_expression(p);
    expect(p, TOK_SEMI);
    ast_node_t *n = ast_new(AST_EXPR_STMT);
    n->a = e;
    return n;
}

ast_node_t *parse_program(parser_t *p) {
    ast_node_t *prog = ast_new(AST_BLOCK);
    ast_node_t *tail = NULL;
    while (!check(p, TOK_EOF) && !p->error) {
        ast_node_t *s = parse_statement(p);
        if (!prog->list) { prog->list = s; } else { tail->next = s; }
        tail = s;
    }
    return prog;
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/parser.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:24:02
Now the tree-walking evaluator:
Write · Jul 17, 19:24:04 · toolu_01WSxD212wn5zLMqoZVbWPtx
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/eval.h
#pragma once

/* Parses and executes a script's source text, printing output to the
 * console + serial as it runs. Parse/runtime errors are reported the
 * same way rather than propagated - there is no caller that needs a
 * status code yet. */
void script_run(const char *src);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/eval.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:24:37 · toolu_01TajjegQ71c5wjL2kRPAnpR
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/eval.c
376 lines
#include "eval.h"

#include "../../console.h"
#include "../../fs/tinyfs.h"
#include "../../memory.h"
#include "../../serial.h"
#include "../../string.h"
#include "ast.h"
#include "parser.h"

typedef enum { VAL_INT, VAL_STR } value_type_t;

typedef struct {
    value_type_t type;
    union {
        int32_t i;
        char *s;
    } as;
} value_t;

#define MAX_VARS 32

typedef struct {
    char names[MAX_VARS][TOKEN_TEXT_MAX];
    value_t values[MAX_VARS];
    int count;
} env_t;

#define MAX_FUNCS 16

typedef struct {
    char name[TOKEN_TEXT_MAX];
    ast_node_t *def;
} func_entry_t;

typedef struct {
    func_entry_t funcs[MAX_FUNCS];
    int func_count;
} interp_t;

typedef enum { EXEC_NORMAL, EXEC_RETURN } exec_signal_t;

typedef struct {
    exec_signal_t signal;
    value_t return_value;
} exec_result_t;

static const value_t VAL_ZERO = { VAL_INT, { .i = 0 } };

static int env_find(env_t *env, const char *name) {
    for (int i = 0; i < env->count; i++) {
        if (strcmp(env->names[i], name) == 0) {
            return i;
        }
    }
    return -1;
}

static value_t env_get(env_t *env, const char *name) {
    int i = env_find(env, name);
    return i < 0 ? VAL_ZERO : env->values[i];
}

static void env_set(env_t *env, const char *name, value_t v) {
    int i = env_find(env, name);
    if (i >= 0) {
        env->values[i] = v;
        return;
    }
    if (env->count < MAX_VARS) {
        int n = 0;
        while (name[n] && n < TOKEN_TEXT_MAX - 1) {
            env->names[env->count][n] = name[n];
            n++;
        }
        env->names[env->count][n] = '\0';
        env->values[env->count] = v;
        env->count++;
    }
    /* else: variable table full - silently dropped, tiny scope */
}

static int to_int(value_t v) {
    if (v.type == VAL_INT) {
        return v.as.i;
    }
    return (v.as.s && v.as.s[0]) ? 1 : 0;
}

static char *str_concat(const char *a, const char *b) {
    size_t la = strlen(a);
    size_t lb = strlen(b);
    char *r = kmalloc(la + lb + 1);
    memcpy(r, a, la);
    memcpy(r + la, b, lb);
    r[la + lb] = '\0';
    return r;
}

static void print_value(value_t v) {
    if (v.type == VAL_STR) {
        console_write(v.as.s);
        serial_write(v.as.s);
    } else {
        char buf[16];
        itoa(v.as.i, buf, 10);
        console_write(buf);
        serial_write(buf);
    }
}

static value_t eval_binop(token_type_t op, value_t l, value_t r) {
    if (op == TOK_PLUS && (l.type == VAL_STR || r.type == VAL_STR)) {
        char lb[16], rb[16];
        const char *ls = (l.type == VAL_STR) ? l.as.s : itoa(l.as.i, lb, 10);
        const char *rs = (r.type == VAL_STR) ? r.as.s : itoa(r.as.i, rb, 10);
        return (value_t){ VAL_STR, { .s = str_concat(ls, rs) } };
    }
    if (op == TOK_EQ || op == TOK_NEQ) {
        int eq;
        if (l.type == VAL_STR && r.type == VAL_STR) {
            eq = strcmp(l.as.s, r.as.s) == 0;
        } else if (l.type == VAL_INT && r.type == VAL_INT) {
            eq = l.as.i == r.as.i;
        } else {
            eq = 0;
        }
        return (value_t){ VAL_INT, { .i = (op == TOK_EQ) ? eq : !eq } };
    }

    int li = to_int(l);
    int ri = to_int(r);
    switch (op) {
        case TOK_MINUS:   return (value_t){ VAL_INT, { .i = li - ri } };
        case TOK_STAR:    return (value_t){ VAL_INT, { .i = li * ri } };
        case TOK_SLASH:   return (value_t){ VAL_INT, { .i = ri != 0 ? li / ri : 0 } };
        case TOK_PERCENT: return (value_t){ VAL_INT, { .i = ri != 0 ? li % ri : 0 } };
        case TOK_LT:      return (value_t){ VAL_INT, { .i = li < ri } };
        case TOK_LE:      return (value_t){ VAL_INT, { .i = li <= ri } };
        case TOK_GT:      return (value_t){ VAL_INT, { .i = li > ri } };
        case TOK_GE:      return (value_t){ VAL_INT, { .i = li >= ri } };
        default:          return VAL_ZERO;
    }
}

static ast_node_t *find_func(interp_t *interp, const char *name) {
    for (int i = 0; i < interp->func_count; i++) {
        if (strcmp(interp->funcs[i].name, name) == 0) {
            return interp->funcs[i].def;
        }
    }
    return NULL;
}

static void register_func(interp_t *interp, ast_node_t *def) {
    if (interp->func_count >= MAX_FUNCS) {
        return;
    }
    func_entry_t *e = &interp->funcs[interp->func_count++];
    int n = 0;
    while (def->name[n] && n < TOKEN_TEXT_MAX - 1) {
        e->name[n] = def->name[n];
        n++;
    }
    e->name[n] = '\0';
    e->def = def;
}

static value_t eval_expr(interp_t *interp, env_t *env, ast_node_t *node);
static exec_result_t exec_stmt(interp_t *interp, env_t *env, ast_node_t *node);

static exec_result_t exec_block(interp_t *interp, env_t *env, ast_node_t *block) {
    exec_result_t r = { EXEC_NORMAL, VAL_ZERO };
    for (ast_node_t *s = block->list; s; s = s->next) {
        r = exec_stmt(interp, env, s);
        if (r.signal == EXEC_RETURN) {
            return r;
        }
    }
    return r;
}

static value_t eval_call(interp_t *interp, env_t *env, ast_node_t *node) {
    value_t args[8];
    int argc = 0;
    for (ast_node_t *a = node->list; a && argc < 8; a = a->next) {
        args[argc++] = eval_expr(interp, env, a);
    }

    if (strcmp(node->name, "print") == 0) {
        for (int i = 0; i < argc; i++) {
            if (i > 0) {
                console_write(" ");
                serial_write(" ");
            }
            print_value(args[i]);
        }
        console_write("\n");
        serial_write("\n");
        return VAL_ZERO;
    }

    if (strcmp(node->name, "fread") == 0 && argc >= 1 && args[0].type == VAL_STR) {
        static char buf[TINYFS_MAX_FILE_SIZE + 1];
        uint32_t len = 0;
        if (tinyfs_read(args[0].as.s, buf, sizeof(buf) - 1, &len) == 0) {
            buf[len] = '\0';
            char *out = kmalloc(len + 1);
            memcpy(out, buf, len + 1);
            return (value_t){ VAL_STR, { .s = out } };
        }
        return (value_t){ VAL_STR, { .s = "" } };
    }

    if (strcmp(node->name, "fwrite") == 0 && argc >= 2 && args[0].type == VAL_STR) {
        const char *content = (args[1].type == VAL_STR) ? args[1].as.s : "";
        tinyfs_create(args[0].as.s); /* ignored if it already exists */
        int rc = tinyfs_write(args[0].as.s, content, (uint32_t)strlen(content));
        return (value_t){ VAL_INT, { .i = rc == 0 } };
    }

    if (strcmp(node->name, "flist") == 0) {
        tinyfs_dirent_t entries[TINYFS_NUM_INODES];
        int n = tinyfs_list(entries, TINYFS_NUM_INODES);
        for (int i = 0; i < n; i++) {
            console_write(entries[i].name);
            console_write("\n");
            serial_write(entries[i].name);
            serial_write("\n");
        }
        return (value_t){ VAL_INT, { .i = n } };
    }

    ast_node_t *def = find_func(interp, node->name);
    if (!def) {
        return VAL_ZERO; /* unknown function - tiny scope, no error propagation */
    }

    env_t local;
    local.count = 0;
    int i = 0;
    for (ast_node_t *pn = def->list; pn; pn = pn->next, i++) {
        env_set(&local, pn->name, i < argc ? args[i] : VAL_ZERO);
    }

    exec_result_t r = exec_block(interp, &local, def->b);
    return r.signal == EXEC_RETURN ? r.return_value : VAL_ZERO;
}

static value_t eval_expr(interp_t *interp, env_t *env, ast_node_t *node) {
    switch (node->kind) {
        case AST_NUM:
            return (value_t){ VAL_INT, { .i = node->num } };
        case AST_STR:
            return (value_t){ VAL_STR, { .s = node->str } };
        case AST_VAR:
            return env_get(env, node->name);
        case AST_ASSIGN: {
            value_t v = eval_expr(interp, env, node->a);
            env_set(env, node->name, v);
            return v;
        }
        case AST_UNOP: {
            value_t v = eval_expr(interp, env, node->a);
            if (node->op == TOK_MINUS) {
                return (value_t){ VAL_INT, { .i = -to_int(v) } };
            }
            return (value_t){ VAL_INT, { .i = !to_int(v) } };
        }
        case AST_BINOP: {
            if (node->op == TOK_AND) {
                value_t l = eval_expr(interp, env, node->a);
                if (!to_int(l)) {
                    return VAL_ZERO;
                }
                value_t r = eval_expr(interp, env, node->b);
                return (value_t){ VAL_INT, { .i = to_int(r) != 0 } };
            }
            if (node->op == TOK_OR) {
                value_t l = eval_expr(interp, env, node->a);
                if (to_int(l)) {
                    return (value_t){ VAL_INT, { .i = 1 } };
                }
                value_t r = eval_expr(interp, env, node->b);
                return (value_t){ VAL_INT, { .i = to_int(r) != 0 } };
            }
            value_t l = eval_expr(interp, env, node->a);
            value_t r = eval_expr(interp, env, node->b);
            return eval_binop(node->op, l, r);
        }
        case AST_CALL:
            return eval_call(interp, env, node);
        default:
            return VAL_ZERO;
    }
}

static exec_result_t exec_stmt(interp_t *interp, env_t *env, ast_node_t *node) {
    exec_result_t normal = { EXEC_NORMAL, VAL_ZERO };

    switch (node->kind) {
        case AST_BLOCK:
            return exec_block(interp, env, node);
        case AST_IF: {
            value_t c = eval_expr(interp, env, node->a);
            if (to_int(c)) {
                return exec_stmt(interp, env, node->b);
            }
            if (node->c) {
                return exec_stmt(interp, env, node->c);
            }
            return normal;
        }
        case AST_WHILE:
            while (to_int(eval_expr(interp, env, node->a))) {
                exec_result_t r = exec_stmt(interp, env, node->b);
                if (r.signal == EXEC_RETURN) {
                    return r;
                }
            }
            return normal;
        case AST_FUNC_DEF:
            /* top-level defs are pre-registered by script_run; a nested
             * func statement (inside a block) registers here instead */
            register_func(interp, node);
            return normal;
        case AST_RETURN: {
            exec_result_t r = { EXEC_RETURN, VAL_ZERO };
            if (node->a) {
                r.return_value = eval_expr(interp, env, node->a);
            }
            return r;
        }
        case AST_EXPR_STMT:
            eval_expr(interp, env, node->a);
            return normal;
        default:
            return normal;
    }
}

void script_run(const char *src) {
    parser_t p;
    parser_init(&p, src);
    ast_node_t *prog = parse_program(&p);

    if (p.error) {
        console_write("script: parse error: ");
        console_write(p.error_msg);
        console_write("\n");
        serial_write("script: parse error: ");
        serial_write(p.error_msg);
        serial_write("\n");
        return;
    }

    interp_t interp;
    interp.func_count = 0;
    env_t global;
    global.count = 0;

    for (ast_node_t *s = prog->list; s; s = s->next) {
        if (s->kind == AST_FUNC_DEF) {
            register_func(&interp, s);
        }
    }
    for (ast_node_t *s = prog->list; s; s = s->next) {
        if (s->kind != AST_FUNC_DEF) {
            exec_result_t r = exec_stmt(&interp, &global, s);
            if (r.signal == EXEC_RETURN) {
                break;
            }
        }
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/eval.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:24:39
Now wire `run <file>` into the shell:
Edit · Jul 17, 19:24:41 · toolu_01BkWWGJBfUeuoqJWWxF1Mcn
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
- #include "console.h"
- #include "fs/tinyfs.h"
- #include "keyboard.h"
- #include "serial.h"
- #include "string.h"
+ #include "console.h"
+ #include "fs/tinyfs.h"
+ #include "keyboard.h"
+ #include "lang/script/eval.h"
+ #include "serial.h"
+ #include "string.h"
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:24:45 · toolu_01GVzwbQEvyuM5evZMoGxeop
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
- static void cmd_rm(const char *name) {
+ static void cmd_run(const char *name) {
+     static char buf[TINYFS_MAX_FILE_SIZE + 1];
+     uint32_t len = 0;
+     if (tinyfs_read(name, buf, sizeof(buf) - 1, &len) != 0) {
+         print_line("run: no such file");
+         return;
+     }
+     buf[len] = '\0';
+     script_run(buf);
+ }
+ 
+ static void cmd_rm(const char *name) {
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:24:51 · toolu_01S1iWr5oz3G2iVmNhKvULEQ
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
-         } else if (strcmp(cmd, "rm") == 0) {
-             char name[TINYFS_NAME_MAX];
-             if (next_word(&p, name, sizeof(name)) == 0) {
-                 print_line("usage: rm <file>");
-             } else {
-                 cmd_rm(name);
-             }
+         } else if (strcmp(cmd, "rm") == 0) {
+             char name[TINYFS_NAME_MAX];
+             if (next_word(&p, name, sizeof(name)) == 0) {
+                 print_line("usage: rm <file>");
+             } else {
+                 cmd_rm(name);
+             }
+         } else if (strcmp(cmd, "run") == 0) {
+             char name[TINYFS_NAME_MAX];
+             if (next_word(&p, name, sizeof(name)) == 0) {
+                 print_line("usage: run <file>");
+             } else {
+                 cmd_run(name);
+             }
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:24:54
Also update the `help` text:
Edit · Jul 17, 19:24:55 · toolu_01EkTdyuPZ2BSj8VAPpZ7BRS
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
-     print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, help, clear, reboot");
+     print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>, help, clear, reboot");
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:25:21
Now seed a `.tsh` fixture and wire mkfs to load seed files from the host at build time.
Read · Jul 17, 19:25:21 · toolu_016GFcjxH6Crp9dezeiTDrV6
/home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c
result
60 lines
1	/* Host-side build tool: formats the tinyfs region of a disk image and
2	 * seeds it with initial files. Not part of the OS - infrastructure, like
3	 * the assembler/linker/QEMU, run at build time on the host. Reuses
4	 * kernel/fs/tinyfs.c verbatim (compiled natively here, freestanding in
5	 * the kernel) so there is exactly one implementation of the fs format. */
6	
7	#include <stdio.h>
8	#include <stdlib.h>
9	#include <string.h>
10	
11	#include "../kernel/fs/tinyfs.h"
12	
13	static FILE *disk;
14	
15	static void disk_block_read(uint32_t block, void *buf) {
16	    long offset = (long)(TINYFS_FS_START_LBA + block) * TINYFS_BLOCK_SIZE;
17	    if (fseek(disk, offset, SEEK_SET) != 0 ||
18	        fread(buf, 1, TINYFS_BLOCK_SIZE, disk) != TINYFS_BLOCK_SIZE) {
19	        fprintf(stderr, "mkfs_tinyfs: read failed at block %u\n", block);
20	        exit(1);
21	    }
22	}
23	
24	static void disk_block_write(uint32_t block, const void *buf) {
25	    long offset = (long)(TINYFS_FS_START_LBA + block) * TINYFS_BLOCK_SIZE;
26	    if (fseek(disk, offset, SEEK_SET) != 0 ||
27	        fwrite(buf, 1, TINYFS_BLOCK_SIZE, disk) != TINYFS_BLOCK_SIZE) {
28	        fprintf(stderr, "mkfs_tinyfs: write failed at block %u\n", block);
29	        exit(1);
30	    }
31	}
32	
33	static void seed_file(const char *name, const char *contents) {
34	    tinyfs_create(name);
35	    tinyfs_write(name, contents, (uint32_t)strlen(contents));
36	}
37	
38	int main(int argc, char **argv) {
39	    if (argc != 2) {
40	        fprintf(stderr, "usage: %s <disk-image>\n", argv[0]);
41	        return 1;
42	    }
43	
44	    disk = fopen(argv[1], "r+b");
45	    if (!disk) {
46	        perror("fopen");
47	        return 1;
48	    }
49	
50	    tinyfs_init(disk_block_read, disk_block_write);
51	    tinyfs_format();
52	
53	    seed_file("hello.txt", "Hello, tinyfs!\n");
54	
55	    fclose(disk);
56	    printf("mkfs_tinyfs: formatted %s (%u data blocks available)\n",
57	           argv[1], (unsigned)TINYFS_DATA_BLOCKS);
58	    return 0;
59	}
60	
Edit · Jul 17, 19:25:32 · toolu_01HXKavtJdujCzDieBy5xGgv
/home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c
89 lines
- static void seed_file(const char *name, const char *contents) {
-     tinyfs_create(name);
-     tinyfs_write(name, contents, (uint32_t)strlen(contents));
- }
- 
- int main(int argc, char **argv) {
-     if (argc != 2) {
-         fprintf(stderr, "usage: %s <disk-image>\n", argv[0]);
-         return 1;
-     }
- 
-     disk = fopen(argv[1], "r+b");
-     if (!disk) {
-         perror("fopen");
-         return 1;
-     }
- 
-     tinyfs_init(disk_block_read, disk_block_write);
-     tinyfs_format();
- 
-     seed_file("hello.txt", "Hello, tinyfs!\n");
- 
-     fclose(disk);
-     printf("mkfs_tinyfs: formatted %s (%u data blocks available)\n",
-            argv[1], (unsigned)TINYFS_DATA_BLOCKS);
-     return 0;
- }
+ static void seed_file(const char *name, const char *contents) {
+     tinyfs_create(name);
+     tinyfs_write(name, contents, (uint32_t)strlen(contents));
+ }
+ 
+ /* Seeds a tinyfs file straight from a host file, for test fixtures
+  * (tests/*.tsh, tests/*.tc) so their source lives in one place instead of
+  * being duplicated as a C string literal here. */
+ static void seed_file_from_host(const char *host_path, const char *fs_name) {
+     FILE *f = fopen(host_path, "rb");
+     if (!f) {
+         fprintf(stderr, "mkfs_tinyfs: cannot open seed file %s\n", host_path);
+         exit(1);
+     }
+     char buf[TINYFS_MAX_FILE_SIZE];
+     size_t n = fread(buf, 1, sizeof(buf), f);
+     fclose(f);
+ 
+     tinyfs_create(fs_name);
+     tinyfs_write(fs_name, buf, (uint32_t)n);
+ }
+ 
+ int main(int argc, char **argv) {
+     if (argc < 2) {
+         fprintf(stderr, "usage: %s <disk-image> [host-path:fs-name ...]\n", argv[0]);
+         return 1;
+     }
+ 
+     disk = fopen(argv[1], "r+b");
+     if (!disk) {
+         perror("fopen");
+         return 1;
+     }
+ 
+     tinyfs_init(disk_block_read, disk_block_write);
+     tinyfs_format();
+ 
+     seed_file("hello.txt", "Hello, tinyfs!\n");
+ 
+     for (int i = 2; i < argc; i++) {
+         char spec[256];
+         size_t speclen = strlen(argv[i]);
+         if (speclen >= sizeof(spec)) {
+             fprintf(stderr, "mkfs_tinyfs: seed spec too long: %s\n", argv[i]);
+             return 1;
+         }
+         memcpy(spec, argv[i], speclen + 1);
+ 
+         char *sep = strchr(spec, ':');
+         if (!sep) {
+             fprintf(stderr, "mkfs_tinyfs: bad seed spec %s (want host-path:fs-name)\n", argv[i]);
+             return 1;
+         }
+         *sep = '\0';
+         seed_file_from_host(spec, sep + 1);
+     }
+ 
+     fclose(disk);
+     printf("mkfs_tinyfs: formatted %s (%u data blocks available)\n",
+            argv[1], (unsigned)TINYFS_DATA_BLOCKS);
+     return 0;
+ }
result
The file /home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:25:34
Now the test fixture and its expected output:
Bash · Jul 17, 19:25:35 · toolu_012XeaTtWWhvvC3vSEYcHYKk
Create tests directories
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/tests/expected
result
(Bash completed with no output)
Write · Jul 17, 19:25:38 · toolu_015GRqtjqCCLdzFtPw2gPQjK
/home/martin/workspace/prototypes/boot-to-claude/tests/hello.tsh
print("hello from tinyfs script");

x = 2 + 3 * 4;
print("x =", x);

func add(a, b) {
    return a + b;
}
print("add(3,4) =", add(3, 4));

i = 0;
while (i < 3) {
    print("loop", i);
    i = i + 1;
}

fwrite("script_out.txt", "written by script");
print(fread("script_out.txt"));
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/hello.tsh (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:25:41 · toolu_01F8BgPuv8KJN2ogyKYuR3Aa
/home/martin/workspace/prototypes/boot-to-claude/tests/expected/hello.tsh.txt
hello from tinyfs script
x = 14
add(3,4) = 7
loop 0
loop 1
loop 2
written by script
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/expected/hello.tsh.txt (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:25:44
Now update the Makefile to pass the seed spec, and update `run_tests.sh` to exercise it:
Edit · Jul 17, 19:25:47 · toolu_01C5eCJL65VnQGutioUKtuyt
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- MKFS_BIN := $(BUILD)/mkfs_tinyfs
- MKFS_OBJS := $(BUILD)/host_tinyfs.o $(BUILD)/host_mkfs_tinyfs.o
+ MKFS_BIN := $(BUILD)/mkfs_tinyfs
+ MKFS_OBJS := $(BUILD)/host_tinyfs.o $(BUILD)/host_mkfs_tinyfs.o
+ MKFS_SEEDS := tests/hello.tsh:hello.tsh
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:25:53 · toolu_01Q93pijpr5p8NKXnzM9SZPX
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- $(DISK_IMG): $(BOOT_BIN) $(STAGE2_BIN) $(KERNEL_BIN) $(MKFS_BIN)
- 	dd if=/dev/zero of=$@ bs=512 count=$(DISK_SECTORS) status=none
- 	dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none
- 	dd if=$(STAGE2_BIN) of=$@ bs=512 seek=1 conv=notrunc status=none
- 	dd if=$(KERNEL_BIN) of=$@ bs=512 seek=$(KERNEL_START_SECTOR) conv=notrunc status=none
- 	$(MKFS_BIN) $@
+ $(DISK_IMG): $(BOOT_BIN) $(STAGE2_BIN) $(KERNEL_BIN) $(MKFS_BIN) $(MKFS_SEEDS:%=%)
+ 	dd if=/dev/zero of=$@ bs=512 count=$(DISK_SECTORS) status=none
+ 	dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none
+ 	dd if=$(STAGE2_BIN) of=$@ bs=512 seek=1 conv=notrunc status=none
+ 	dd if=$(KERNEL_BIN) of=$@ bs=512 seek=$(KERNEL_START_SECTOR) conv=notrunc status=none
+ 	$(MKFS_BIN) $@ $(MKFS_SEEDS)
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:26:03 · toolu_0183B989XuHA1jwpHn5UbBDM
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- MKFS_BIN := $(BUILD)/mkfs_tinyfs
- MKFS_OBJS := $(BUILD)/host_tinyfs.o $(BUILD)/host_mkfs_tinyfs.o
- MKFS_SEEDS := tests/hello.tsh:hello.tsh
+ MKFS_BIN := $(BUILD)/mkfs_tinyfs
+ MKFS_OBJS := $(BUILD)/host_tinyfs.o $(BUILD)/host_mkfs_tinyfs.o
+ # host-path:fs-name pairs seeded into every disk image; the host paths
+ # alone are listed separately so they can be used as make prerequisites
+ MKFS_SEED_FILES := tests/hello.tsh
+ MKFS_SEED_SPECS := tests/hello.tsh:hello.tsh
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:26:16 · toolu_013qC21qdLdZnEboJx1A5itk
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- $(DISK_IMG): $(BOOT_BIN) $(STAGE2_BIN) $(KERNEL_BIN) $(MKFS_BIN) $(MKFS_SEEDS:%=%)
- 	dd if=/dev/zero of=$@ bs=512 count=$(DISK_SECTORS) status=none
- 	dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none
- 	dd if=$(STAGE2_BIN) of=$@ bs=512 seek=1 conv=notrunc status=none
- 	dd if=$(KERNEL_BIN) of=$@ bs=512 seek=$(KERNEL_START_SECTOR) conv=notrunc status=none
- 	$(MKFS_BIN) $@ $(MKFS_SEEDS)
+ $(DISK_IMG): $(BOOT_BIN) $(STAGE2_BIN) $(KERNEL_BIN) $(MKFS_BIN) $(MKFS_SEED_FILES)
+ 	dd if=/dev/zero of=$@ bs=512 count=$(DISK_SECTORS) status=none
+ 	dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none
+ 	dd if=$(STAGE2_BIN) of=$@ bs=512 seek=1 conv=notrunc status=none
+ 	dd if=$(KERNEL_BIN) of=$@ bs=512 seek=$(KERNEL_START_SECTOR) conv=notrunc status=none
+ 	$(MKFS_BIN) $@ $(MKFS_SEED_SPECS)
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:26:19
Now add the new kernel sources and build directories to the Makefile:
Edit · Jul 17, 19:26:21 · toolu_017K1jbE3AVeWBFq2TJq847y
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \
-                  kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \
-                  kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \
-                  kernel/keyboard.c kernel/shell/shell.c
+ KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \
+                  kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \
+                  kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \
+                  kernel/keyboard.c kernel/shell/shell.c \
+                  kernel/lang/lexer.c kernel/lang/script/ast.c \
+                  kernel/lang/script/parser.c kernel/lang/script/eval.c
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:26:31 · toolu_0171Hm7ACWqsEvR6j8fb72C7
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- $(BUILD) $(BUILD)/fs $(BUILD)/shell:
- 	mkdir -p $@
- 
- $(BOOT_BIN): boot/boot.S | $(BUILD)
- 	$(AS) -f bin -o $@ $<
- 
- $(STAGE2_BIN): boot/stage2.S | $(BUILD)
- 	$(AS) -f bin -o $@ $<
- 
- $(BUILD)/%.o: kernel/%.S | $(BUILD) $(BUILD)/fs $(BUILD)/shell
- 	$(AS) -f elf32 -o $@ $<
- 
- $(BUILD)/%.o: kernel/%.c | $(BUILD) $(BUILD)/fs $(BUILD)/shell
- 	$(CC) $(CFLAGS) -c -o $@ $<
+ BUILD_SUBDIRS := $(BUILD) $(BUILD)/fs $(BUILD)/shell $(BUILD)/lang $(BUILD)/lang/script
+ 
+ $(BUILD_SUBDIRS):
+ 	mkdir -p $@
+ 
+ $(BOOT_BIN): boot/boot.S | $(BUILD)
+ 	$(AS) -f bin -o $@ $<
+ 
+ $(STAGE2_BIN): boot/stage2.S | $(BUILD)
+ 	$(AS) -f bin -o $@ $<
+ 
+ $(BUILD)/%.o: kernel/%.S | $(BUILD_SUBDIRS)
+ 	$(AS) -f elf32 -o $@ $<
+ 
+ $(BUILD)/%.o: kernel/%.c | $(BUILD_SUBDIRS)
+ 	$(CC) $(CFLAGS) -c -o $@ $<
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:26:34
Now update `run_tests.sh` with the fixture-driven check:
Edit · Jul 17, 19:26:36 · toolu_01MrNy7CVvFR8VE865fYSjPG
/home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh
- type_line() {
-     python3 tools/qemu_type.py "$MON" "$1"$'\n'
- }
+ type_line() {
+     python3 tools/qemu_type.py "$MON" "$1"$'\n'
+ }
+ 
+ # Checks that every non-blank line in tests/expected/<fixture> appears
+ # (in order) somewhere in the serial log - used for .tsh/.tc fixtures run
+ # through the shell's `run`/`build`+`exec` commands.
+ check_fixture_output() {
+     local expected_file="$1"
+     while IFS= read -r want; do
+         [ -z "$want" ] && continue
+         wait_for "$want" 30
+     done < "$expected_file"
+ }
result
The file /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:26:40 · toolu_016pV7FENbFA67WqKog729Kv
/home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh
- type_line "help"
- wait_for "commands:"
- 
- echo "run_tests: PASS"
+ type_line "help"
+ wait_for "commands:"
+ 
+ type_line "run hello.tsh"
+ check_fixture_output tests/expected/hello.tsh.txt
+ 
+ echo "run_tests: PASS"
result
The file /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh has been updated successfully. (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:26:44 · toolu_01VGZdp6xw7aC9LfqEPJpXv5
Rebuild with Phase 5 scripting language additions
$ make clean >/dev/null && make 2>&1
result
41 lines
mkdir -p build
nasm -f bin -o build/boot.bin boot/boot.S
nasm -f bin -o build/stage2.bin boot/stage2.S
mkdir -p build/fs
mkdir -p build/shell
mkdir -p build/lang
mkdir -p build/lang/script
nasm -f elf32 -o build/entry.o kernel/entry.S
nasm -f elf32 -o build/idt_stubs.o kernel/idt_stubs.S
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/kmain.o kernel/kmain.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/console.o kernel/console.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/serial.o kernel/serial.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/gdt.o kernel/gdt.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/idt.o kernel/idt.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/string.o kernel/string.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/memory.o kernel/memory.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/selftest.o kernel/selftest.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/ata.o kernel/ata.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/fs/tinyfs.o kernel/fs/tinyfs.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/fs/tinyfs_disk.o kernel/fs/tinyfs_disk.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/keyboard.o kernel/keyboard.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/shell/shell.o kernel/shell/shell.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/lexer.o kernel/lang/lexer.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/script/ast.o kernel/lang/script/ast.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/script/parser.o kernel/lang/script/parser.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/script/eval.o kernel/lang/script/eval.c
ld -m elf_i386 -T kernel/link.ld -nostdlib -o build/kernel.elf build/entry.o build/idt_stubs.o build/kmain.o build/console.o build/serial.o build/gdt.o build/idt.o build/string.o build/memory.o build/selftest.o build/ata.o build/fs/tinyfs.o build/fs/tinyfs_disk.o build/keyboard.o build/shell/shell.o build/lang/lexer.o build/lang/script/ast.o build/lang/script/parser.o build/lang/script/eval.o
ld: warning: build/kernel.elf has a LOAD segment with RWX permissions
objcopy -O binary build/kernel.elf build/kernel.bin
gcc -Wall -Wextra -O2 -c -o build/host_tinyfs.o kernel/fs/tinyfs.c
gcc -Wall -Wextra -O2 -c -o build/host_mkfs_tinyfs.o tools/mkfs_tinyfs.c
tools/mkfs_tinyfs.c:39:10: warning: ‘/*’ within comment [-Wcomment]
   39 |  * (tests/*.tsh, tests/*.tc) so their source lives in one place instead of
tools/mkfs_tinyfs.c:39:23: warning: ‘/*’ within comment [-Wcomment]
gcc -o build/mkfs_tinyfs build/host_tinyfs.o build/host_mkfs_tinyfs.o
dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none
dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none
build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh
mkfs_tinyfs: formatted build/disk.img (3958 data blocks available)
agent · Jul 17, 19:26:49
Clean kernel build; fixing a minor comment-nesting warning in the host tool.
Edit · Jul 17, 19:26:51 · toolu_01T8LdvJkjv8yRF8bp2gfJqS
/home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c
- /* Seeds a tinyfs file straight from a host file, for test fixtures
-  * (tests/*.tsh, tests/*.tc) so their source lives in one place instead of
-  * being duplicated as a C string literal here. */
+ /* Seeds a tinyfs file straight from a host file, for test fixtures under
+  * tests/ so their source lives in one place instead of being duplicated
+  * as a C string literal here. */
result
The file /home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c has been updated successfully. (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:26:53 · toolu_013PEmKRSW5iNvVmoVDH3jU8
Check kernel size and run full test harness including the .tsh fixture
$ stat -c%s build/kernel.bin && ./tools/run_tests.sh
result (error)
1385 lines
Exit code 1
28428
run_tests: FAIL - timed out waiting for 'x = 14'
--- serial log ---
tiny-os: kernel entered
selftest: running
SELFTEST PASS
tiny-os shell. type 'help' for commands.
> ls
hello.txt  15
hello.tsh  281
> cat hello.txt
Hello, tinyfs!
> write greet.txt hi there
write: ok
> cat greet.txt
hi there
> rm greet.txt
rm: ok
> cat greet.txt
cat: no such file
> help
commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>, help, clear, reboot
> run hello.tsh
hello from tinyfs script
x = 0
add(3,4) = 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop

... [20011 characters truncated] ...

op 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop 0
loop
agent · Jul 17, 19:27:51
Found the bug: `eval_binop` never handles `TOK_PLUS` for two ints — only the string-concat path checks it, so int+int silently falls through to `default: return VAL_ZERO`. That explains `x=0`, `add()=0`, and the infinite loop (`i = i + 1` always yielding 0). Fixing and checking for stray QEMU processes first.
Bash · Jul 17, 19:27:52 · toolu_01UR2TsNkZcg9nREqi5PpHKT
Check for leftover qemu processes from the failed run
$ pgrep -af qemu-system-i386 || echo "no stray qemu processes"
result
1888311 /bin/bash -c source /home/martin/.claude/shell-snapshots/snapshot-bash-1784307599835-hlkzgk.sh 2>/dev/null || true && shopt -u extglob 2>/dev/null || true && eval 'pgrep -af qemu-system-i386 || echo "no stray qemu processes"' < /dev/null && pwd -P >| /tmp/claude-f695-cwd
agent · Jul 17, 19:27:54
Clean, no stray QEMU. Now fixing the bug:
Edit · Jul 17, 19:27:56 · toolu_01Fwv2GuqwaKBBiMH6v3XX8H
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/eval.c
-     int li = to_int(l);
-     int ri = to_int(r);
-     switch (op) {
-         case TOK_MINUS:   return (value_t){ VAL_INT, { .i = li - ri } };
+     int li = to_int(l);
+     int ri = to_int(r);
+     switch (op) {
+         case TOK_PLUS:    return (value_t){ VAL_INT, { .i = li + ri } };
+         case TOK_MINUS:   return (value_t){ VAL_INT, { .i = li - ri } };
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/script/eval.c has been updated successfully. (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:28:03 · toolu_01FUfugPVJHDjU55XBbGsiFP
Rebuild with the fix and rerun the full test harness
$ make 2>&1 | tail -5 && ./tools/run_tests.sh 2>&1 | tail -40
result
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none
dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none
build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh
mkfs_tinyfs: formatted build/disk.img (3958 data blocks available)
run_tests: PASS
qemu-system-i386: terminating on signal 15 from pid 1888423 (bash)
agent · Jul 17, 19:28:13
All tests pass, including function calls, arithmetic, while loops, and fread/fwrite. Checking kernel size budget before committing.
Bash · Jul 17, 19:28:14 · toolu_014xFhQiH5Q6txjKBp18J6u6
Verify kernel binary still fits the 32KB/64-sector budget
$ stat -c%s build/kernel.bin
result
28452
agent · Jul 17, 19:28:34
The kernel is at 28452/32768 bytes (87% of budget) and Phase 6 will add more. Let me increase headroom now and add a build-time size guard rather than risk silent truncation.
Read · Jul 17, 19:28:35 · toolu_01WT42SoBeHkrLXNAaVHecht
/home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S
result
1	; Stage 2 bootloader. Loaded by stage1 at 0x0000:0x7e00, still 16-bit real mode.
2	; Job: enable A20, load the kernel from disk into memory below 1MB, build a
3	; flat GDT, switch to 32-bit protected mode, and jump into the kernel entry.
4	
5	BITS 16
6	ORG 0x7e00
7	
8	KERNEL_LOAD_ADDR equ 0x10000  ; 64KB - plenty of conventional memory headroom
9	KERNEL_START_LBA equ 5        ; right after stage1 (LBA 0) + stage2 (LBA 1-4)
10	KERNEL_SECTORS   equ 64       ; 32KB kernel image budget for now
11	
12	stage2_start:
13	    mov si, msg_stage2
14	    call print_str16
15	
Edit · Jul 17, 19:28:42 · toolu_01N4zSRy9KmUWG2urcn4JyvY
/home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S
- KERNEL_LOAD_ADDR equ 0x10000  ; 64KB - plenty of conventional memory headroom
- KERNEL_START_LBA equ 5        ; right after stage1 (LBA 0) + stage2 (LBA 1-4)
- KERNEL_SECTORS   equ 64       ; 32KB kernel image budget for now
+ KERNEL_LOAD_ADDR equ 0x10000  ; 64KB - plenty of conventional memory headroom
+ KERNEL_START_LBA equ 5        ; right after stage1 (LBA 0) + stage2 (LBA 1-4)
+ KERNEL_SECTORS   equ 512      ; 256KB kernel image budget (must match Makefile/tinyfs.h)
result
The file /home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:28:47 · toolu_01Ei3Wrd5A1MGsa1a7EZm2Cu
/home/martin/workspace/prototypes/boot-to-claude/kernel/fs/tinyfs.h
- #define TINYFS_DISK_SECTORS 4096
- #define TINYFS_FS_START_LBA 128 /* leaves 64KB for boot sector + kernel */
+ #define TINYFS_DISK_SECTORS 4096
+ #define TINYFS_FS_START_LBA 640 /* leaves room for boot sector + a 256KB kernel budget */
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/fs/tinyfs.h has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:28:49
Now updating the Makefile with the matching constant plus a build-time size guard:
Read · Jul 17, 19:28:50 · toolu_01KhSv3UstSxSVeqso8tn8k2
/home/martin/workspace/prototypes/boot-to-claude/Makefile
result
90 lines
1	AS      := nasm
2	CC      := gcc
3	HOSTCC  := gcc
4	LD      := ld
5	OBJCOPY := objcopy
6	QEMU    := qemu-system-i386
7	BUILD   := build
8	
9	CFLAGS  := -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib \
10	           -Wall -Wextra -O2 -std=c11 -Ikernel
11	LDFLAGS := -m elf_i386 -T kernel/link.ld -nostdlib
12	
13	# Must match TINYFS_DISK_SECTORS / TINYFS_FS_START_LBA in kernel/fs/tinyfs.h
14	DISK_SECTORS  := 4096
15	KERNEL_START_SECTOR := 5
16	
17	BOOT_BIN   := $(BUILD)/boot.bin
18	STAGE2_BIN := $(BUILD)/stage2.bin
19	DISK_IMG   := $(BUILD)/disk.img
20	
21	KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \
22	                 kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \
23	                 kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \
24	                 kernel/keyboard.c kernel/shell/shell.c \
25	                 kernel/lang/lexer.c kernel/lang/script/ast.c \
26	                 kernel/lang/script/parser.c kernel/lang/script/eval.c
27	KERNEL_ASM_SRCS := kernel/entry.S kernel/idt_stubs.S
28	KERNEL_OBJS   := $(patsubst kernel/%.S,$(BUILD)/%.o,$(KERNEL_ASM_SRCS)) \
29	                 $(patsubst kernel/%.c,$(BUILD)/%.o,$(KERNEL_C_SRCS))
30	KERNEL_ELF    := $(BUILD)/kernel.elf
31	KERNEL_BIN    := $(BUILD)/kernel.bin
32	
33	MKFS_BIN := $(BUILD)/mkfs_tinyfs
34	MKFS_OBJS := $(BUILD)/host_tinyfs.o $(BUILD)/host_mkfs_tinyfs.o
35	# host-path:fs-name pairs seeded into every disk image; the host paths
36	# alone are listed separately so they can be used as make prerequisites
37	MKFS_SEED_FILES := tests/hello.tsh
38	MKFS_SEED_SPECS := tests/hello.tsh:hello.tsh
39	
40	.PHONY: all run clean
41	
42	all: $(DISK_IMG)
43	
44	BUILD_SUBDIRS := $(BUILD) $(BUILD)/fs $(BUILD)/shell $(BUILD)/lang $(BUILD)/lang/script
45	
46	$(BUILD_SUBDIRS):
47		mkdir -p $@
48	
49	$(BOOT_BIN): boot/boot.S | $(BUILD)
50		$(AS) -f bin -o $@ $<
51	
52	$(STAGE2_BIN): boot/stage2.S | $(BUILD)
53		$(AS) -f bin -o $@ $<
54	
55	$(BUILD)/%.o: kernel/%.S | $(BUILD_SUBDIRS)
56		$(AS) -f elf32 -o $@ $<
57	
58	$(BUILD)/%.o: kernel/%.c | $(BUILD_SUBDIRS)
59		$(CC) $(CFLAGS) -c -o $@ $<
60	
61	$(KERNEL_ELF): $(KERNEL_OBJS) kernel/link.ld
62		$(LD) $(LDFLAGS) -o $@ $(KERNEL_OBJS)
63	
64	$(KERNEL_BIN): $(KERNEL_ELF)
65		$(OBJCOPY) -O binary $< $@
66	
67	# --- host-side mkfs tool (build-time infrastructure, not kernel code) ---
68	
69	$(BUILD)/host_tinyfs.o: kernel/fs/tinyfs.c | $(BUILD)
70		$(HOSTCC) -Wall -Wextra -O2 -c -o $@ $<
71	
72	$(BUILD)/host_mkfs_tinyfs.o: tools/mkfs_tinyfs.c | $(BUILD)
73		$(HOSTCC) -Wall -Wextra -O2 -c -o $@ $<
74	
75	$(MKFS_BIN): $(MKFS_OBJS)
76		$(HOSTCC) -o $@ $(MKFS_OBJS)
77	
78	$(DISK_IMG): $(BOOT_BIN) $(STAGE2_BIN) $(KERNEL_BIN) $(MKFS_BIN) $(MKFS_SEED_FILES)
79		dd if=/dev/zero of=$@ bs=512 count=$(DISK_SECTORS) status=none
80		dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none
81		dd if=$(STAGE2_BIN) of=$@ bs=512 seek=1 conv=notrunc status=none
82		dd if=$(KERNEL_BIN) of=$@ bs=512 seek=$(KERNEL_START_SECTOR) conv=notrunc status=none
83		$(MKFS_BIN) $@ $(MKFS_SEED_SPECS)
84	
85	run: $(DISK_IMG)
86		$(QEMU) -drive file=$(DISK_IMG),format=raw -display none -serial stdio -no-reboot
87	
88	clean:
89		rm -rf $(BUILD)
90	
Edit · Jul 17, 19:28:54 · toolu_01ShrC27XvwSm8dLqdnYV3Dc
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- # Must match TINYFS_DISK_SECTORS / TINYFS_FS_START_LBA in kernel/fs/tinyfs.h
- DISK_SECTORS  := 4096
- KERNEL_START_SECTOR := 5
+ # Must match TINYFS_DISK_SECTORS / TINYFS_FS_START_LBA in kernel/fs/tinyfs.h
+ # and KERNEL_START_LBA / KERNEL_SECTORS in boot/stage2.S
+ DISK_SECTORS  := 4096
+ KERNEL_START_SECTOR := 5
+ KERNEL_MAX_SECTORS  := 512
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:28:59 · toolu_01Kgfs9bE3wtSH8Eof9tHkmS
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- $(KERNEL_BIN): $(KERNEL_ELF)
- 	$(OBJCOPY) -O binary $< $@
+ $(KERNEL_BIN): $(KERNEL_ELF)
+ 	$(OBJCOPY) -O binary $< $@
+ 	@size=$$(stat -c%s $@); budget=$$(( $(KERNEL_MAX_SECTORS) * 512 )); \
+ 	if [ "$$size" -gt "$$budget" ]; then \
+ 		echo "error: $@ is $$size bytes, exceeds the $$budget-byte ($(KERNEL_MAX_SECTORS)-sector) boot budget" >&2; \
+ 		echo "       bump KERNEL_SECTORS in boot/stage2.S, KERNEL_MAX_SECTORS here, and TINYFS_FS_START_LBA in kernel/fs/tinyfs.h together" >&2; \
+ 		exit 1; \
+ 	fi
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:29:01
Rebuild from scratch and rerun the full test suite to confirm everything still works with the new layout:
Bash · Jul 17, 19:29:02 · toolu_01DBLbrRcmkSTkruNN15gWsf
Full rebuild with updated disk layout constants
$ make clean >/dev/null && make 2>&1 | tail -20
result
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/fs/tinyfs.o kernel/fs/tinyfs.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/fs/tinyfs_disk.o kernel/fs/tinyfs_disk.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/keyboard.o kernel/keyboard.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/shell/shell.o kernel/shell/shell.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/lexer.o kernel/lang/lexer.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/script/ast.o kernel/lang/script/ast.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/script/parser.o kernel/lang/script/parser.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/script/eval.o kernel/lang/script/eval.c
ld -m elf_i386 -T kernel/link.ld -nostdlib -o build/kernel.elf build/entry.o build/idt_stubs.o build/kmain.o build/console.o build/serial.o build/gdt.o build/idt.o build/string.o build/memory.o build/selftest.o build/ata.o build/fs/tinyfs.o build/fs/tinyfs_disk.o build/keyboard.o build/shell/shell.o build/lang/lexer.o build/lang/script/ast.o build/lang/script/parser.o build/lang/script/eval.o
ld: warning: build/kernel.elf has a LOAD segment with RWX permissions
objcopy -O binary build/kernel.elf build/kernel.bin
gcc -Wall -Wextra -O2 -c -o build/host_tinyfs.o kernel/fs/tinyfs.c
gcc -Wall -Wextra -O2 -c -o build/host_mkfs_tinyfs.o tools/mkfs_tinyfs.c
gcc -o build/mkfs_tinyfs build/host_tinyfs.o build/host_mkfs_tinyfs.o
dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none
dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none
build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh
mkfs_tinyfs: formatted build/disk.img (3446 data blocks available)
agent · Jul 17, 19:29:07
Build succeeds with the size guard in place, and mkfs reports the expected reduced-but-ample capacity. Running full test suite:
Bash · Jul 17, 19:29:07 · toolu_01CJgyN4D3T2eFZrDSZXcFEE
Run full test suite after disk layout change
$ ./tools/run_tests.sh 2>&1 | tail -10
result
run_tests: FAIL - timed out waiting for 'SELFTEST PASS'
--- serial log ---
qemu-system-i386: terminating on signal 15 from pid 1889181 (bash)
Bash · Jul 17, 19:29:23 · toolu_017ruxSbwiCyjAVvr3R1tmz5
Boot manually to diagnose the regression, capturing CPU exceptions
$ timeout 5 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial stdio -no-reboot -d int -D /tmp/qemu.log 2>&1; echo "---exceptions---"; grep -m5 "v=" /tmp/qemu.log 2>/dev/null
result
qemu-system-i386: terminating on signal 15 from pid 1889407 (timeout)
---exceptions---
agent · Jul 17, 19:30:16
Root cause is likely SeaBIOS's per-call sector limit on `INT13h AH=0x42` (single-shot reads of ~512 sectors are unreliable; most BIOSes cap around 127-128). The correct fix is a chunked read loop rather than picking a smaller magic number that will bite again in Phase 6. Let me rewrite `load_kernel` in stage2.
Read · Jul 17, 19:30:17 · toolu_01DXJv6N8BxF9A7mXDiN5Gzu
/home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S
result
112 lines
1	; Stage 2 bootloader. Loaded by stage1 at 0x0000:0x7e00, still 16-bit real mode.
2	; Job: enable A20, load the kernel from disk into memory below 1MB, build a
3	; flat GDT, switch to 32-bit protected mode, and jump into the kernel entry.
4	
5	BITS 16
6	ORG 0x7e00
7	
8	KERNEL_LOAD_ADDR equ 0x10000  ; 64KB - plenty of conventional memory headroom
9	KERNEL_START_LBA equ 5        ; right after stage1 (LBA 0) + stage2 (LBA 1-4)
10	KERNEL_SECTORS   equ 512      ; 256KB kernel image budget (must match Makefile/tinyfs.h)
11	
12	stage2_start:
13	    mov si, msg_stage2
14	    call print_str16
15	
16	    call enable_a20
17	    call load_kernel
18	
19	    cli
20	    lgdt [gdt_descriptor]
21	
22	    mov eax, cr0
23	    or eax, 1
24	    mov cr0, eax
25	
26	    jmp CODE_SEG:protected_mode_entry
27	
28	; ---------------- 16-bit helpers ----------------
29	
30	print_str16:
31	    pusha
32	.loop:
33	    lodsb
34	    or al, al
35	    jz .done
36	    mov ah, 0x0e
37	    mov bh, 0
38	    int 0x10
39	    jmp .loop
40	.done:
41	    popa
42	    ret
43	
44	enable_a20:
45	    in al, 0x92
46	    or al, 2
47	    out 0x92, al
48	    ret
49	
50	load_kernel:
51	    mov si, dap
52	    mov ah, 0x42
53	    mov dl, 0x80
54	    int 0x13
55	    jc disk_error
56	    ret
57	
58	disk_error:
59	    mov si, msg_disk_err
60	    call print_str16
61	.hang:
62	    hlt
63	    jmp .hang
64	
65	align 4
66	dap:
67	    db 0x10
68	    db 0
69	    dw KERNEL_SECTORS
70	    dw KERNEL_LOAD_ADDR & 0xFFFF          ; offset  (0x0000)
71	    dw (KERNEL_LOAD_ADDR >> 4) & 0xFFFF   ; segment (0x1000)
72	    dq KERNEL_START_LBA
73	
74	msg_stage2:   db "stage2 ", 0
75	msg_disk_err: db "kernel disk err", 0
76	
77	; ---------------- GDT: flat 4GB code/data ----------------
78	
79	align 8
80	gdt_start:
81	    dq 0                            ; null descriptor
82	gdt_code:
83	    dw 0xFFFF, 0
84	    db 0, 10011010b, 11001111b, 0
85	gdt_data:
86	    dw 0xFFFF, 0
87	    db 0, 10010010b, 11001111b, 0
88	gdt_end:
89	
90	gdt_descriptor:
91	    dw gdt_end - gdt_start - 1
92	    dd gdt_start
93	
94	CODE_SEG equ gdt_code - gdt_start
95	DATA_SEG equ gdt_data - gdt_start
96	
97	; ---------------- 32-bit protected mode entry ----------------
98	
99	BITS 32
100	protected_mode_entry:
101	    mov ax, DATA_SEG
102	    mov ds, ax
103	    mov es, ax
104	    mov fs, ax
105	    mov gs, ax
106	    mov ss, ax
107	    mov esp, 0x90000
108	
109	    jmp KERNEL_LOAD_ADDR
110	
111	times 2048-($-$$) db 0   ; pad to STAGE2_SECTORS (4) * 512 bytes
112	
Edit · Jul 17, 19:30:29 · toolu_01LZSMY4CVQoLWyCrMTKZqD2
/home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S
96 lines
- load_kernel:
-     mov si, dap
-     mov ah, 0x42
-     mov dl, 0x80
-     int 0x13
-     jc disk_error
-     ret
- 
- disk_error:
-     mov si, msg_disk_err
-     call print_str16
- .hang:
-     hlt
-     jmp .hang
- 
- align 4
- dap:
-     db 0x10
-     db 0
-     dw KERNEL_SECTORS
-     dw KERNEL_LOAD_ADDR & 0xFFFF          ; offset  (0x0000)
-     dw (KERNEL_LOAD_ADDR >> 4) & 0xFFFF   ; segment (0x1000)
-     dq KERNEL_START_LBA
- 
- msg_stage2:   db "stage2 ", 0
- msg_disk_err: db "kernel disk err", 0
+ ; Reads the kernel in CHUNK_SECTORS-sized pieces rather than one single
+ ; INT13h AH=0x42 call - BIOSes (including SeaBIOS) commonly cap how many
+ ; sectors a single extended read will reliably transfer well below 512,
+ ; and silently misbehave past that rather than returning an error.
+ CHUNK_SECTORS equ 64
+ 
+ load_kernel:
+     mov word [kernel_remaining], KERNEL_SECTORS
+     mov dword [kernel_lba], KERNEL_START_LBA
+     mov word [kernel_dest_seg], (KERNEL_LOAD_ADDR >> 4)
+ 
+ .loop:
+     mov ax, [kernel_remaining]
+     cmp ax, 0
+     je .done
+     cmp ax, CHUNK_SECTORS
+     jbe .chunk_size_ok
+     mov ax, CHUNK_SECTORS
+ .chunk_size_ok:
+     ; ax = sectors to transfer this iteration
+ 
+     mov [dap_sectors], ax
+     mov si, [kernel_dest_seg]
+     mov [dap_segment], si
+     mov word [dap_offset], 0
+     mov ebx, [kernel_lba]
+     mov [dap_lba], ebx
+     mov dword [dap_lba + 4], 0
+ 
+     push ax
+     mov si, dap
+     mov ah, 0x42
+     mov dl, 0x80
+     int 0x13
+     jc disk_error
+     pop ax
+ 
+     movzx eax, ax                  ; sectors transferred, zero-extended
+     add dword [kernel_lba], eax
+     sub word [kernel_remaining], ax
+     mov cx, ax
+     shl cx, 5                       ; sectors * (512/16) = paragraphs advanced
+     add word [kernel_dest_seg], cx
+ 
+     jmp .loop
+ .done:
+     ret
+ 
+ disk_error:
+     mov si, msg_disk_err
+     call print_str16
+ .hang:
+     hlt
+     jmp .hang
+ 
+ kernel_remaining: dw 0
+ kernel_lba:       dd 0
+ kernel_dest_seg:  dw 0
+ 
+ align 4
+ dap:
+ dap_size:    db 0x10
+              db 0
+ dap_sectors: dw 0
+ dap_offset:  dw 0
+ dap_segment: dw 0
+ dap_lba:     dq 0
+ 
+ msg_stage2:   db "stage2 ", 0
+ msg_disk_err: db "kernel disk err", 0
result
The file /home/martin/workspace/prototypes/boot-to-claude/boot/stage2.S has been updated successfully. (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:30:32 · toolu_01BAKR8XhENJJeDfH8wtZPfU
Rebuild with chunked kernel loading and rerun test suite
$ make 2>&1 | tail -10 && ./tools/run_tests.sh 2>&1 | tail -20
result
nasm -f bin -o build/stage2.bin boot/stage2.S
dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none
dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none
build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh
mkfs_tinyfs: formatted build/disk.img (3446 data blocks available)
run_tests: PASS
qemu-system-i386: terminating on signal 15 from pid 1892564 (bash)
agent · Jul 17, 19:30:42
Confirmed and fixed — chunked BIOS disk reads resolved it, full test suite passes. Committing Phase 5.
Bash · Jul 17, 19:30:42 · toolu_0188w3x8Rcz5vU8kzTWkwmet
Stage Phase 5 files
$ git add -A && git status --short
result
M  Makefile
M  boot/stage2.S
M  kernel/fs/tinyfs.h
A  kernel/lang/lexer.c
A  kernel/lang/lexer.h
A  kernel/lang/script/ast.c
A  kernel/lang/script/ast.h
A  kernel/lang/script/eval.c
A  kernel/lang/script/eval.h
A  kernel/lang/script/parser.c
A  kernel/lang/script/parser.h
M  kernel/shell/shell.c
A  tests/expected/hello.tsh.txt
A  tests/hello.tsh
M  tools/mkfs_tinyfs.c
M  tools/run_tests.sh