Phase 6: compiler, bytecode VM

Bash · Jul 17, 19:30:51 · toolu_01XrHM8QAiMx58eTcMRLrNMh
Commit Phase 5 scripting language
$ git commit -m "$(cat <<'EOF'
Phase 5: scripting language (tree-walking interpreter)

Adds a shared tokenizer (kernel/lang/lexer.c, reused by the Phase 6
compiler) and a dynamic scripting language on top of it: recursive-
descent parser, generic AST, tree-walking evaluator. Supports ints,
strings (+ concatenation), variables, if/else, while, user-defined
functions, and OS builtins (print, fread, fwrite, flist) backed by
tinyfs. Shell gains `run <file>`.

Fixes a real bug caught by the new fixture test: eval_binop handled
TOK_PLUS only in the string-concat branch, so int+int silently fell
through to `default: return 0` - making every arithmetic sum evaluate
to 0 and turning `i = i + 1` into an infinite loop.

Also fixes a boot regression hit while growing the kernel size budget
(64 -> 512 sectors) to make room for this phase: stage2's single
INT13h AH=0x42 read of the whole kernel silently failed once the
transfer grew past whatever SeaBIOS's per-call sector limit is.
load_kernel now reads in bounded 64-sector chunks, which is the
correct general fix rather than a fragile smaller magic number.
TINYFS_FS_START_LBA moves 128 -> 640 to match the larger kernel
budget, and the Makefile now fails the build loudly instead of
silently truncating if the kernel image ever exceeds its sector
budget again.

tests/hello.tsh + tests/expected/hello.tsh.txt exercise the language
end-to-end via the shell's `run` command in tools/run_tests.sh.
EOF
)"
result
[main f4702ba] Phase 5: scripting language (tree-walking interpreter)
 16 files changed, 1126 insertions(+), 17 deletions(-)
 create mode 100644 kernel/lang/lexer.c
 create mode 100644 kernel/lang/lexer.h
 create mode 100644 kernel/lang/script/ast.c
 create mode 100644 kernel/lang/script/ast.h
 create mode 100644 kernel/lang/script/eval.c
 create mode 100644 kernel/lang/script/eval.h
 create mode 100644 kernel/lang/script/parser.c
 create mode 100644 kernel/lang/script/parser.h
 create mode 100644 tests/expected/hello.tsh.txt
 create mode 100644 tests/hello.tsh
TaskUpdate · Jul 17, 19:30:54 · toolu_01KgaFCDVFXijmukHRpqTHUH
{
 "taskId": "6",
 "status": "completed"
}
result
Updated task #6 status
TaskUpdate · Jul 17, 19:30:54 · toolu_01DFhx9D3WKjZ2DLp5QaB2Fj
{
 "taskId": "7",
 "status": "in_progress"
}
result
Updated task #7 status
agent · Jul 17, 19:34:03
Now designing the bytecode ISA and writing the doc first, then implementing.
Write · Jul 17, 19:34:19 · toolu_01YFRxXc1dKdbUoutYAdY2xW
/home/martin/workspace/prototypes/boot-to-claude/docs/bytecode-isa.md
94 lines
# tiny compiler bytecode ISA

A stack machine, similar in spirit to tinyfs: minimal, fixed-size, no
dynamic growth. Values are 32-bit signed integers only - the compiled
language (unlike the scripting language) has no string type.

## Design invariant

Every expression, once compiled, leaves **exactly one** value on the value
stack. Statement-level wrappers (`OP_POP` after an expression-statement)
and binary/unary ops (pop N, push 1) all rely on this. Assignment is an
expression too: it evaluates to the assigned value, via `<value> DUP
STORE_*` rather than a plain `STORE_*` that would leave nothing behind.

## VM state

Four fixed-size arrays, no heap allocation at run time:

- **value stack** - expression operands/results.
- **locals stack** - one contiguous region per active call frame (params
  then other locals, slot-indexed - names are resolved to slots at compile
  time, the VM has no notion of variable names).
- **call stack** - `{return_pc, locals_base}` per active call, for nested
  and recursive calls.
- **globals** - fixed array, slot-indexed, for variables assigned outside
  any function.

Functions cannot see globals or an enclosing scope - same "no closures"
rule as the scripting language's interpreter (kernel/lang/script/eval.c),
so both languages behave consistently.

## Opcodes

| op | value | operand | stack effect |
|---|---|---|---|
| `OP_HALT` | 0 | - | stops execution |
| `OP_PUSH_CONST` | 1 | i32 | push const |
| `OP_POP` | 2 | - | pop |
| `OP_DUP` | 3 | - | push top again |
| `OP_LOAD_LOCAL` | 4 | u8 slot | push locals[base+slot] |
| `OP_STORE_LOCAL` | 5 | u8 slot | pop -> locals[base+slot] |
| `OP_LOAD_GLOBAL` | 6 | u8 slot | push globals[slot] |
| `OP_STORE_GLOBAL` | 7 | u8 slot | pop -> globals[slot] |
| `OP_ADD`..`OP_OR` | 8-20 | - | pop 2, push 1 (`+ - * / % == != < <= > >= && \|\|`) |
| `OP_NEG` | 21 | - | pop 1, push 1 |
| `OP_NOT` | 22 | - | pop 1, push 1 |
| `OP_JMP` | 23 | u16 addr | `pc = addr` |
| `OP_JMP_IF_FALSE` | 24 | u16 addr | pop cond; if false, `pc = addr` |
| `OP_CALL` | 25 | u16 func index | pop `param_count` args, push new frame, jump to entry |
| `OP_RET` | 26 | - | pop return value, pop frame, push return value back |
| `OP_SYS_PRINT` | 27 | - | pop 1, print as decimal + newline, push 0 |

`OP_AND`/`OP_OR` are eager (both operands always evaluated), unlike the
scripting language's short-circuit `&&`/`||` - a deliberate simplification
for the compiled path.

Division/modulo by zero yields 0 rather than trapping, matching the
scripting language's behavior.

## File format (`.tbc`)

```
struct tbc_header {        // 12 bytes, packed
    uint32_t magic;         // 'T','B','C','1'
    uint16_t entry_pc;       // where top-level code starts, within code[]
    uint16_t global_count;
    uint16_t func_count;
    uint16_t code_size;
};

struct tbc_func {          // 4 bytes, packed, func_count of these follow the header
    uint16_t entry_addr;    // offset within code[]
    uint8_t  param_count;
    uint8_t  local_count;   // includes params, which occupy slots 0..param_count-1
};

uint8_t code[code_size];   // follows the func table
```

The whole file must fit in one tinyfs file (`TINYFS_MAX_FILE_SIZE`, 4KB).

## Source language

C-like and statically scoped: `int`-only variables (no declarations -
first assignment creates the variable, in whichever scope - global or
current function - is active), `if`/`else`, `while`, `func name(params)
{...}` with `return`, arithmetic/comparison/logical operators, and one
builtin, `print(expr)`. Parser: `kernel/lang/compiler/parser.c`, reusing
the scripting language's AST node type (`kernel/lang/script/ast.h`) since
the tree shape is identical minus string literals. Codegen:
`kernel/lang/compiler/codegen.c`. VM: `kernel/lang/vm.c`.

Shell usage: `build prog.tc prog.tbc` compiles, `exec prog.tbc` runs it.
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/docs/bytecode-isa.md (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:34:21
Now the shared bytecode header definitions:
Write · Jul 17, 19:34:26 · toolu_01KtNQd1PnCkys92Yf5QszGv
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/bytecode.h
56 lines
#pragma once

/* Shared between kernel/lang/compiler/codegen.c (producer) and
 * kernel/lang/vm.c (consumer). See docs/bytecode-isa.md. */

#include <stdint.h>

#define TBC_MAGIC 0x31434254u /* "TBC1" */

typedef enum {
    OP_HALT = 0,
    OP_PUSH_CONST,
    OP_POP,
    OP_DUP,
    OP_LOAD_LOCAL,
    OP_STORE_LOCAL,
    OP_LOAD_GLOBAL,
    OP_STORE_GLOBAL,
    OP_ADD,
    OP_SUB,
    OP_MUL,
    OP_DIV,
    OP_MOD,
    OP_EQ,
    OP_NEQ,
    OP_LT,
    OP_LE,
    OP_GT,
    OP_GE,
    OP_AND,
    OP_OR,
    OP_NEG,
    OP_NOT,
    OP_JMP,
    OP_JMP_IF_FALSE,
    OP_CALL,
    OP_RET,
    OP_SYS_PRINT,
} bc_opcode_t;

typedef struct {
    uint32_t magic;
    uint16_t entry_pc;
    uint16_t global_count;
    uint16_t func_count;
    uint16_t code_size;
} __attribute__((packed)) tbc_header_t;

typedef struct {
    uint16_t entry_addr;
    uint8_t  param_count;
    uint8_t  local_count;
} __attribute__((packed)) tbc_func_t;

#define VM_FUNCS_MAX 16
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/bytecode.h (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:34:29
Now the compiler's parser (near-identical to the script parser, minus string literals):
Bash · Jul 17, 19:34:29 · toolu_01CnfmsxZb8CfkjU6tPjfZ23
Create compiler directory
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler
result
(Bash completed with no output)
Write · Jul 17, 19:34:38 · toolu_018EXdgxCyxdcXmCWEWfh9xv
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/parser.h
#pragma once

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

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

void tc_parser_init(tc_parser_t *p, const char *src);

/* Parses the whole source as a top-level AST_BLOCK of statements. Check
 * p->error afterwards. Unlike the scripting language's parser, string
 * literals are a parse error here - the compiled language is int-only. */
ast_node_t *tc_parse_program(tc_parser_t *p);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/parser.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:35:02 · toolu_01NmNjE6ZMuJRhEHSVKrVJ5a
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/parser.c
309 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(tc_parser_t *p) {
    p->cur = lexer_next(&p->lexer);
}

void tc_parser_init(tc_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(tc_parser_t *p, const char *msg) {
    if (!p->error) {
        p->error = 1;
        copy_name(p->error_msg, msg);
    }
}

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

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

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

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

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

static ast_node_t *parse_block(tc_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(tc_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)) {
        parse_error(p, "strings are not supported in the compiled language");
        advance(p);
        return ast_new(AST_NUM);
    }
    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);
    return ast_new(AST_NUM);
}

static ast_node_t *parse_unary(tc_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(tc_parser_t *p, ast_node_t *(*next)(tc_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(tc_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(tc_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(tc_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(tc_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(tc_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(tc_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(tc_parser_t *p) {
    if (check(p, TOK_IDENT)) {
        tc_parser_t save = *p;
        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);
            ast_node_t *n = ast_new(AST_ASSIGN);
            copy_name(n->name, name);
            n->a = value;
            return n;
        }
        *p = save;
    }
    return parse_or(p);
}

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

static ast_node_t *parse_statement(tc_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 *tc_parse_program(tc_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/compiler/parser.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:35:08
Now the codegen:
Write · Jul 17, 19:35:10 · toolu_01TLVdLjb9HGF4MrwjhWGopg
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/codegen.h
#pragma once

#include <stdint.h>

#include "../script/ast.h"

/* Compiles a parsed program into a .tbc byte buffer (see
 * docs/bytecode-isa.md). Returns 0 on success and fills *out_len; on
 * failure returns -1 and fills error_msg. out_buf must be at least
 * out_buf_size bytes. */
int codegen_compile(ast_node_t *prog, uint8_t *out_buf, uint32_t out_buf_size,
                     uint32_t *out_len, char *error_msg, uint32_t error_msg_size);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/codegen.h (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:35:16 · toolu_01ByLYSHtDsiMpp8caxrYfw1
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/bytecode.h
- #define VM_FUNCS_MAX 16
+ #define VM_FUNCS_MAX 16
+ #define VM_GLOBALS_MAX 32
+ #define VM_LOCALS_PER_FRAME_MAX 32 /* param_count + other locals, per function */
+ #define VM_MAX_CALL_ARGS 8
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/bytecode.h has been updated successfully. (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:35:49 · toolu_01PRa8RVNtA6WTA2ACDU9xrL
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/codegen.c
393 lines
#include "codegen.h"

#include "../../string.h"
#include "../bytecode.h"
#include "../lexer.h"

typedef struct {
    uint8_t *code;
    uint32_t code_cap;
    uint32_t code_len;

    char func_names[VM_FUNCS_MAX][TOKEN_TEXT_MAX];
    tbc_func_t funcs[VM_FUNCS_MAX];
    int func_count;

    char global_names[VM_GLOBALS_MAX][TOKEN_TEXT_MAX];
    int global_count;

    char local_names[VM_LOCALS_PER_FRAME_MAX][TOKEN_TEXT_MAX];
    int local_count;
    int in_function;

    int error;
    char error_msg[64];
} compiler_t;

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 compile_error(compiler_t *c, const char *msg) {
    if (!c->error) {
        c->error = 1;
        copy_name(c->error_msg, msg); /* error_msg is 64 bytes, plenty for these */
    }
}

static void emit_byte(compiler_t *c, uint8_t b) {
    if (c->code_len >= c->code_cap) {
        compile_error(c, "compiled program too large");
        return;
    }
    c->code[c->code_len++] = b;
}

static void emit_op(compiler_t *c, bc_opcode_t op) {
    emit_byte(c, (uint8_t)op);
}

static void emit_u16(compiler_t *c, uint16_t v) {
    emit_byte(c, (uint8_t)(v & 0xFF));
    emit_byte(c, (uint8_t)((v >> 8) & 0xFF));
}

static void emit_i32(compiler_t *c, int32_t v) {
    uint32_t u = (uint32_t)v;
    emit_byte(c, (uint8_t)(u & 0xFF));
    emit_byte(c, (uint8_t)((u >> 8) & 0xFF));
    emit_byte(c, (uint8_t)((u >> 16) & 0xFF));
    emit_byte(c, (uint8_t)((u >> 24) & 0xFF));
}

static void patch_u16(compiler_t *c, uint32_t pos, uint16_t v) {
    c->code[pos] = (uint8_t)(v & 0xFF);
    c->code[pos + 1] = (uint8_t)((v >> 8) & 0xFF);
}

static int find_local(compiler_t *c, const char *name) {
    for (int i = 0; i < c->local_count; i++) {
        if (strcmp(c->local_names[i], name) == 0) {
            return i;
        }
    }
    return -1;
}

static int find_or_add_local(compiler_t *c, const char *name) {
    int i = find_local(c, name);
    if (i >= 0) {
        return i;
    }
    if (c->local_count >= VM_LOCALS_PER_FRAME_MAX) {
        compile_error(c, "too many local variables in one function");
        return 0;
    }
    copy_name(c->local_names[c->local_count], name);
    return c->local_count++;
}

static int find_global(compiler_t *c, const char *name) {
    for (int i = 0; i < c->global_count; i++) {
        if (strcmp(c->global_names[i], name) == 0) {
            return i;
        }
    }
    return -1;
}

static int find_or_add_global(compiler_t *c, const char *name) {
    int i = find_global(c, name);
    if (i >= 0) {
        return i;
    }
    if (c->global_count >= VM_GLOBALS_MAX) {
        compile_error(c, "too many global variables");
        return 0;
    }
    copy_name(c->global_names[c->global_count], name);
    return c->global_count++;
}

static int find_func_index(compiler_t *c, const char *name) {
    for (int i = 0; i < c->func_count; i++) {
        if (strcmp(c->func_names[i], name) == 0) {
            return i;
        }
    }
    return -1;
}

static void compile_expr(compiler_t *c, ast_node_t *node);
static void compile_stmt(compiler_t *c, ast_node_t *node);

static void compile_var_load(compiler_t *c, const char *name) {
    if (c->in_function) {
        int slot = find_local(c, name);
        if (slot < 0) {
            compile_error(c, "undefined local variable");
            slot = 0;
        }
        emit_op(c, OP_LOAD_LOCAL);
        emit_byte(c, (uint8_t)slot);
    } else {
        int slot = find_global(c, name);
        if (slot < 0) {
            compile_error(c, "undefined global variable");
            slot = 0;
        }
        emit_op(c, OP_LOAD_GLOBAL);
        emit_byte(c, (uint8_t)slot);
    }
}

static void compile_var_store(compiler_t *c, const char *name) {
    if (c->in_function) {
        int slot = find_or_add_local(c, name);
        emit_op(c, OP_STORE_LOCAL);
        emit_byte(c, (uint8_t)slot);
    } else {
        int slot = find_or_add_global(c, name);
        emit_op(c, OP_STORE_GLOBAL);
        emit_byte(c, (uint8_t)slot);
    }
}

static const bc_opcode_t BINOP_TABLE[] = {
    [TOK_PLUS] = OP_ADD, [TOK_MINUS] = OP_SUB, [TOK_STAR] = OP_MUL,
    [TOK_SLASH] = OP_DIV, [TOK_PERCENT] = OP_MOD,
    [TOK_EQ] = OP_EQ, [TOK_NEQ] = OP_NEQ,
    [TOK_LT] = OP_LT, [TOK_LE] = OP_LE, [TOK_GT] = OP_GT, [TOK_GE] = OP_GE,
    [TOK_AND] = OP_AND, [TOK_OR] = OP_OR,
};

static void compile_expr(compiler_t *c, ast_node_t *node) {
    switch (node->kind) {
        case AST_NUM:
            emit_op(c, OP_PUSH_CONST);
            emit_i32(c, node->num);
            return;
        case AST_STR:
            compile_error(c, "strings are not supported in the compiled language");
            return;
        case AST_VAR:
            compile_var_load(c, node->name);
            return;
        case AST_ASSIGN:
            compile_expr(c, node->a);
            emit_op(c, OP_DUP); /* assignment evaluates to the assigned value */
            compile_var_store(c, node->name);
            return;
        case AST_UNOP:
            compile_expr(c, node->a);
            emit_op(c, node->op == TOK_MINUS ? OP_NEG : OP_NOT);
            return;
        case AST_BINOP: {
            compile_expr(c, node->a);
            compile_expr(c, node->b);
            if ((size_t)node->op >= sizeof(BINOP_TABLE) / sizeof(BINOP_TABLE[0])) {
                compile_error(c, "unsupported operator");
                return;
            }
            emit_op(c, BINOP_TABLE[node->op]);
            return;
        }
        case AST_CALL: {
            int argc = 0;
            for (ast_node_t *a = node->list; a; a = a->next) {
                compile_expr(c, a);
                argc++;
            }
            if (strcmp(node->name, "print") == 0) {
                if (argc != 1) {
                    compile_error(c, "print takes exactly one argument");
                }
                emit_op(c, OP_SYS_PRINT);
                return;
            }
            int idx = find_func_index(c, node->name);
            if (idx < 0) {
                compile_error(c, "call to unknown function");
                return;
            }
            if (argc != c->funcs[idx].param_count) {
                compile_error(c, "argument count mismatch");
                return;
            }
            if (argc > VM_MAX_CALL_ARGS) {
                compile_error(c, "too many arguments");
                return;
            }
            emit_op(c, OP_CALL);
            emit_u16(c, (uint16_t)idx);
            return;
        }
        default:
            compile_error(c, "expression expected");
            return;
    }
}

static void compile_stmt(compiler_t *c, ast_node_t *node) {
    switch (node->kind) {
        case AST_BLOCK:
            for (ast_node_t *s = node->list; s; s = s->next) {
                compile_stmt(c, s);
            }
            return;
        case AST_IF: {
            compile_expr(c, node->a);
            emit_op(c, OP_JMP_IF_FALSE);
            uint32_t else_jump = c->code_len;
            emit_u16(c, 0);

            compile_stmt(c, node->b);

            if (node->c) {
                emit_op(c, OP_JMP);
                uint32_t end_jump = c->code_len;
                emit_u16(c, 0);

                patch_u16(c, else_jump, (uint16_t)c->code_len);
                compile_stmt(c, node->c);
                patch_u16(c, end_jump, (uint16_t)c->code_len);
            } else {
                patch_u16(c, else_jump, (uint16_t)c->code_len);
            }
            return;
        }
        case AST_WHILE: {
            uint32_t loop_start = c->code_len;
            compile_expr(c, node->a);
            emit_op(c, OP_JMP_IF_FALSE);
            uint32_t exit_jump = c->code_len;
            emit_u16(c, 0);

            compile_stmt(c, node->b);
            emit_op(c, OP_JMP);
            emit_u16(c, (uint16_t)loop_start);

            patch_u16(c, exit_jump, (uint16_t)c->code_len);
            return;
        }
        case AST_RETURN:
            if (node->a) {
                compile_expr(c, node->a);
            } else {
                emit_op(c, OP_PUSH_CONST);
                emit_i32(c, 0);
            }
            emit_op(c, OP_RET);
            return;
        case AST_EXPR_STMT:
            compile_expr(c, node->a);
            emit_op(c, OP_POP);
            return;
        case AST_FUNC_DEF:
            compile_error(c, "nested functions are not supported");
            return;
        default:
            compile_error(c, "statement expected");
            return;
    }
}

static void compile_function(compiler_t *c, ast_node_t *def) {
    int idx = find_func_index(c, def->name);
    tbc_func_t *fe = &c->funcs[idx];
    fe->entry_addr = (uint16_t)c->code_len;

    c->in_function = 1;
    c->local_count = 0;

    int param_count = 0;
    for (ast_node_t *pn = def->list; pn; pn = pn->next) {
        find_or_add_local(c, pn->name);
        param_count++;
    }
    fe->param_count = (uint8_t)param_count;

    compile_stmt(c, def->b);

    /* Implicit `return 0;` if the body falls through without one. */
    emit_op(c, OP_PUSH_CONST);
    emit_i32(c, 0);
    emit_op(c, OP_RET);

    fe->local_count = (uint8_t)c->local_count;
    c->in_function = 0;
}

int codegen_compile(ast_node_t *prog, uint8_t *out_buf, uint32_t out_buf_size,
                     uint32_t *out_len, char *error_msg, uint32_t error_msg_size) {
    static uint8_t code_buf[4096];

    compiler_t c;
    memset(&c, 0, sizeof(c));
    c.code = code_buf;
    c.code_cap = sizeof(code_buf);

    for (ast_node_t *s = prog->list; s; s = s->next) {
        if (s->kind == AST_FUNC_DEF) {
            if (c.func_count >= VM_FUNCS_MAX) {
                compile_error(&c, "too many functions");
                break;
            }
            copy_name(c.func_names[c.func_count], s->name);
            c.func_count++;
        }
    }

    for (ast_node_t *s = prog->list; s && !c.error; s = s->next) {
        if (s->kind == AST_FUNC_DEF) {
            compile_function(&c, s);
        }
    }

    uint16_t entry_pc = (uint16_t)c.code_len;
    c.in_function = 0;
    for (ast_node_t *s = prog->list; s && !c.error; s = s->next) {
        if (s->kind != AST_FUNC_DEF) {
            compile_stmt(&c, s);
        }
    }
    emit_op(&c, OP_HALT);

    if (c.error) {
        copy_name(error_msg, c.error_msg);
        (void)error_msg_size;
        return -1;
    }

    tbc_header_t hdr;
    hdr.magic = TBC_MAGIC;
    hdr.entry_pc = entry_pc;
    hdr.global_count = (uint16_t)c.global_count;
    hdr.func_count = (uint16_t)c.func_count;
    hdr.code_size = (uint16_t)c.code_len;

    uint32_t total = sizeof(hdr) + (uint32_t)c.func_count * sizeof(tbc_func_t) + c.code_len;
    if (total > out_buf_size) {
        copy_name(error_msg, "compiled program too large for output buffer");
        return -1;
    }

    uint32_t off = 0;
    memcpy(out_buf + off, &hdr, sizeof(hdr));
    off += sizeof(hdr);
    for (int i = 0; i < c.func_count; i++) {
        memcpy(out_buf + off, &c.funcs[i], sizeof(tbc_func_t));
        off += sizeof(tbc_func_t);
    }
    memcpy(out_buf + off, c.code, c.code_len);
    off += c.code_len;

    *out_len = off;
    return 0;
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/compiler/codegen.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:36:01
Now the VM:
Write · Jul 17, 19:36:03 · toolu_018wjfvNjsBLTuren1MYMfq4
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/vm.h
#pragma once

#include <stdint.h>

/* Executes a compiled .tbc program (see docs/bytecode-isa.md). Reports
 * errors to console+serial the same way script_run does. */
void vm_run(const uint8_t *bytecode, uint32_t len);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/vm.h (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:36:09 · toolu_01VuTm73Uw6avCv6ps3ecNKp
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/bytecode.h
- #define VM_FUNCS_MAX 16
- #define VM_GLOBALS_MAX 32
- #define VM_LOCALS_PER_FRAME_MAX 32 /* param_count + other locals, per function */
- #define VM_MAX_CALL_ARGS 8
+ #define VM_FUNCS_MAX 16
+ #define VM_GLOBALS_MAX 32
+ #define VM_LOCALS_PER_FRAME_MAX 32 /* param_count + other locals, per function */
+ #define VM_MAX_CALL_ARGS 8
+ 
+ /* vm.c run-time limits */
+ #define VM_VALUE_STACK_MAX 256
+ #define VM_LOCALS_STACK_MAX 512 /* sum of live frames' local_count */
+ #define VM_CALL_STACK_MAX 32
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/bytecode.h has been updated successfully. (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:36:34 · toolu_01XZEEDAsTdPjZmfJajQMWMe
/home/martin/workspace/prototypes/boot-to-claude/kernel/lang/vm.c
243 lines
#include "vm.h"

#include "../console.h"
#include "../serial.h"
#include "../string.h"
#include "bytecode.h"

typedef struct {
    uint16_t return_pc;
    uint16_t locals_base;
} vm_frame_t;

typedef struct {
    int32_t value_stack[VM_VALUE_STACK_MAX];
    int sp;

    int32_t locals[VM_LOCALS_STACK_MAX];
    int locals_top;
    int locals_base;

    vm_frame_t call_stack[VM_CALL_STACK_MAX];
    int call_depth;

    int32_t globals[VM_GLOBALS_MAX];
} vm_t;

static void report_error(const char *msg) {
    console_write("vm: error: ");
    console_write(msg);
    console_write("\n");
    serial_write("vm: error: ");
    serial_write(msg);
    serial_write("\n");
}

static void vm_push(vm_t *vm, int32_t v) {
    if (vm->sp >= VM_VALUE_STACK_MAX) {
        return; /* stack overflow - silently clamp, tiny scope */
    }
    vm->value_stack[vm->sp++] = v;
}

static int32_t vm_pop(vm_t *vm) {
    if (vm->sp <= 0) {
        return 0;
    }
    return vm->value_stack[--vm->sp];
}

void vm_run(const uint8_t *file, uint32_t len) {
    if (len < sizeof(tbc_header_t)) {
        report_error("truncated .tbc file");
        return;
    }

    tbc_header_t hdr;
    memcpy(&hdr, file, sizeof(hdr));
    if (hdr.magic != TBC_MAGIC) {
        report_error("bad .tbc magic");
        return;
    }
    if (hdr.func_count > VM_FUNCS_MAX) {
        report_error("too many functions for this VM");
        return;
    }

    uint32_t off = sizeof(hdr);
    tbc_func_t funcs[VM_FUNCS_MAX];
    for (int i = 0; i < hdr.func_count; i++) {
        if (off + sizeof(tbc_func_t) > len) {
            report_error("truncated .tbc function table");
            return;
        }
        memcpy(&funcs[i], file + off, sizeof(tbc_func_t));
        off += sizeof(tbc_func_t);
    }

    if (off + hdr.code_size > len) {
        report_error("truncated .tbc code section");
        return;
    }
    const uint8_t *code = file + off;
    uint32_t code_size = hdr.code_size;

    static vm_t vm;
    memset(&vm, 0, sizeof(vm));

    uint32_t pc = hdr.entry_pc;

    for (;;) {
        if (pc >= code_size) {
            report_error("program counter ran off the end of code");
            return;
        }
        uint8_t op = code[pc++];

        switch ((bc_opcode_t)op) {
            case OP_HALT:
                return;

            case OP_PUSH_CONST: {
                int32_t v;
                memcpy(&v, code + pc, sizeof(v));
                pc += sizeof(v);
                vm_push(&vm, v);
                break;
            }
            case OP_POP:
                vm_pop(&vm);
                break;
            case OP_DUP: {
                int32_t v = vm.sp > 0 ? vm.value_stack[vm.sp - 1] : 0;
                vm_push(&vm, v);
                break;
            }
            case OP_LOAD_LOCAL: {
                uint8_t slot = code[pc++];
                vm_push(&vm, vm.locals[vm.locals_base + slot]);
                break;
            }
            case OP_STORE_LOCAL: {
                uint8_t slot = code[pc++];
                vm.locals[vm.locals_base + slot] = vm.sp > 0 ? vm.value_stack[vm.sp - 1] : 0;
                vm_pop(&vm);
                break;
            }
            case OP_LOAD_GLOBAL: {
                uint8_t slot = code[pc++];
                vm_push(&vm, vm.globals[slot]);
                break;
            }
            case OP_STORE_GLOBAL: {
                uint8_t slot = code[pc++];
                vm.globals[slot] = vm.sp > 0 ? vm.value_stack[vm.sp - 1] : 0;
                vm_pop(&vm);
                break;
            }

            case OP_ADD: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a + b); break; }
            case OP_SUB: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a - b); break; }
            case OP_MUL: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a * b); break; }
            case OP_DIV: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, b != 0 ? a / b : 0); break; }
            case OP_MOD: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, b != 0 ? a % b : 0); break; }
            case OP_EQ:  { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a == b); break; }
            case OP_NEQ: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a != b); break; }
            case OP_LT:  { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a < b); break; }
            case OP_LE:  { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a <= b); break; }
            case OP_GT:  { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a > b); break; }
            case OP_GE:  { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, a >= b); break; }
            case OP_AND: { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, (a != 0) && (b != 0)); break; }
            case OP_OR:  { int32_t b = vm_pop(&vm), a = vm_pop(&vm); vm_push(&vm, (a != 0) || (b != 0)); break; }
            case OP_NEG: { int32_t a = vm_pop(&vm); vm_push(&vm, -a); break; }
            case OP_NOT: { int32_t a = vm_pop(&vm); vm_push(&vm, !a); break; }

            case OP_JMP: {
                uint16_t addr;
                memcpy(&addr, code + pc, sizeof(addr));
                pc = addr;
                break;
            }
            case OP_JMP_IF_FALSE: {
                uint16_t addr;
                memcpy(&addr, code + pc, sizeof(addr));
                pc += sizeof(addr);
                int32_t cond = vm_pop(&vm);
                if (!cond) {
                    pc = addr;
                }
                break;
            }

            case OP_CALL: {
                uint16_t func_idx;
                memcpy(&func_idx, code + pc, sizeof(func_idx));
                pc += sizeof(func_idx);

                if (func_idx >= hdr.func_count) {
                    report_error("call to invalid function index");
                    return;
                }
                tbc_func_t *fe = &funcs[func_idx];

                int32_t args[VM_MAX_CALL_ARGS];
                int argc = fe->param_count;
                for (int i = argc - 1; i >= 0; i--) {
                    args[i] = vm_pop(&vm);
                }

                if (vm.call_depth >= VM_CALL_STACK_MAX) {
                    report_error("call stack overflow");
                    return;
                }
                vm.call_stack[vm.call_depth].return_pc = (uint16_t)pc;
                vm.call_stack[vm.call_depth].locals_base = (uint16_t)vm.locals_base;
                vm.call_depth++;

                if (vm.locals_top + fe->local_count > VM_LOCALS_STACK_MAX) {
                    report_error("locals stack overflow");
                    return;
                }
                int new_base = vm.locals_top;
                for (int i = 0; i < fe->local_count; i++) {
                    vm.locals[new_base + i] = (i < argc) ? args[i] : 0;
                }
                vm.locals_base = new_base;
                vm.locals_top = new_base + fe->local_count;

                pc = fe->entry_addr;
                break;
            }
            case OP_RET: {
                int32_t retval = vm_pop(&vm);
                if (vm.call_depth <= 0) {
                    report_error("return with no active call frame");
                    return;
                }
                vm.call_depth--;
                vm.locals_top = vm.locals_base;
                pc = vm.call_stack[vm.call_depth].return_pc;
                vm.locals_base = vm.call_stack[vm.call_depth].locals_base;
                vm_push(&vm, retval);
                break;
            }

            case OP_SYS_PRINT: {
                int32_t v = vm_pop(&vm);
                char buf[16];
                itoa(v, buf, 10);
                console_write(buf);
                console_write("\n");
                serial_write(buf);
                serial_write("\n");
                vm_push(&vm, 0);
                break;
            }

            default:
                report_error("invalid opcode");
                return;
        }
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/lang/vm.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:36:39
Now wire `build`/`exec` into the shell:
Read · Jul 17, 19:36:39 · toolu_0133EUNDHTsyUqo54uSkfk3b
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
result
213 lines
1	#include "shell.h"
2	
3	#include <stdint.h>
4	
5	#include "console.h"
6	#include "fs/tinyfs.h"
7	#include "keyboard.h"
8	#include "lang/script/eval.h"
9	#include "serial.h"
10	#include "string.h"
11	
12	#define LINE_MAX 128
13	
14	static inline void outb(uint16_t port, uint8_t val) {
15	    __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
16	}
17	
18	static inline uint8_t inb(uint16_t port) {
19	    uint8_t ret;
20	    __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
21	    return ret;
22	}
23	
24	static void echo_char(char c) {
25	    console_putc(c);
26	    serial_putc(c);
27	}
28	
29	static void print_line(const char *s) {
30	    console_write(s);
31	    console_write("\n");
32	    serial_write(s);
33	    serial_write("\n");
34	}
35	
36	static int read_line(char *buf, int max_len) {
37	    int len = 0;
38	    for (;;) {
39	        char c = keyboard_getchar();
40	        if (c == '\n') {
41	            echo_char('\n');
42	            buf[len] = '\0';
43	            return len;
44	        } else if (c == '\b') {
45	            if (len > 0) {
46	                len--;
47	                echo_char('\b');
48	                echo_char(' ');
49	                echo_char('\b');
50	            }
51	        } else if (c >= 32 && c < 127 && len < max_len - 1) {
52	            buf[len++] = c;
53	            echo_char(c);
54	        }
55	    }
56	}
57	
58	static void skip_spaces(const char **p) {
59	    while (**p == ' ') {
60	        (*p)++;
61	    }
62	}
63	
64	static int next_word(const char **p, char *out, int max_len) {
65	    skip_spaces(p);
66	    int n = 0;
67	    while (**p && **p != ' ' && n < max_len - 1) {
68	        out[n++] = *(*p)++;
69	    }
70	    out[n] = '\0';
71	    return n;
72	}
73	
74	static void cmd_help(void) {
75	    print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>, help, clear, reboot");
76	}
77	
78	static void cmd_ls(void) {
79	    tinyfs_dirent_t entries[TINYFS_NUM_INODES];
80	    int n = tinyfs_list(entries, TINYFS_NUM_INODES);
81	    char numbuf[16];
82	    for (int i = 0; i < n; i++) {
83	        console_write(entries[i].name);
84	        console_write("  ");
85	        console_write(itoa((int)entries[i].size, numbuf, 10));
86	        console_write("\n");
87	        serial_write(entries[i].name);
88	        serial_write("  ");
89	        serial_write(itoa((int)entries[i].size, numbuf, 10));
90	        serial_write("\n");
91	    }
92	}
93	
94	static void cmd_cat(const char *name) {
95	    static char buf[TINYFS_MAX_FILE_SIZE + 1];
96	    uint32_t len = 0;
97	    if (tinyfs_read(name, buf, sizeof(buf) - 1, &len) != 0) {
98	        print_line("cat: no such file");
99	        return;
100	    }
101	    buf[len] = '\0';
102	    console_write(buf);
103	    serial_write(buf);
104	    if (len == 0 || buf[len - 1] != '\n') {
105	        console_write("\n");
106	        serial_write("\n");
107	    }
108	}
109	
110	static void cmd_write(const char *name, const char *contents) {
111	    tinyfs_create(name); /* ignored if it already exists - write() below overwrites it */
112	    uint32_t len = (uint32_t)strlen(contents);
113	    if (len > TINYFS_MAX_FILE_SIZE) {
114	        print_line("write: content too long");
115	        return;
116	    }
117	    if (tinyfs_write(name, contents, len) != 0) {
118	        print_line("write: failed (no space or bad name)");
119	        return;
120	    }
121	    print_line("write: ok");
122	}
123	
124	static void cmd_run(const char *name) {
125	    static char buf[TINYFS_MAX_FILE_SIZE + 1];
126	    uint32_t len = 0;
127	    if (tinyfs_read(name, buf, sizeof(buf) - 1, &len) != 0) {
128	        print_line("run: no such file");
129	        return;
130	    }
131	    buf[len] = '\0';
132	    script_run(buf);
133	}
134	
135	static void cmd_rm(const char *name) {
136	    if (tinyfs_delete(name) != 0) {
137	        print_line("rm: no such file");
138	    } else {
139	        print_line("rm: ok");
140	    }
141	}
142	
143	static void cmd_reboot(void) {
144	    print_line("rebooting...");
145	    uint8_t status;
146	    do {
147	        status = inb(0x64);
148	    } while (status & 0x02);
149	    outb(0x64, 0xFE); /* pulse the keyboard controller's reset line */
150	    for (;;) {
151	        __asm__ volatile ("hlt");
152	    }
153	}
154	
155	void shell_run(void) {
156	    static char line[LINE_MAX];
157	
158	    print_line("tiny-os shell. type 'help' for commands.");
159	
160	    for (;;) {
161	        console_write("> ");
162	        serial_write("> ");
163	        read_line(line, sizeof(line));
164	
165	        const char *p = line;
166	        char cmd[32];
167	        if (next_word(&p, cmd, sizeof(cmd)) == 0) {
168	            continue;
169	        }
170	
171	        if (strcmp(cmd, "help") == 0) {
172	            cmd_help();
173	        } else if (strcmp(cmd, "ls") == 0) {
174	            cmd_ls();
175	        } else if (strcmp(cmd, "clear") == 0) {
176	            console_clear();
177	        } else if (strcmp(cmd, "reboot") == 0) {
178	            cmd_reboot();
179	        } else if (strcmp(cmd, "cat") == 0) {
180	            char name[TINYFS_NAME_MAX];
181	            if (next_word(&p, name, sizeof(name)) == 0) {
182	                print_line("usage: cat <file>");
183	            } else {
184	                cmd_cat(name);
185	            }
186	        } else if (strcmp(cmd, "rm") == 0) {
187	            char name[TINYFS_NAME_MAX];
188	            if (next_word(&p, name, sizeof(name)) == 0) {
189	                print_line("usage: rm <file>");
190	            } else {
191	                cmd_rm(name);
192	            }
193	        } else if (strcmp(cmd, "run") == 0) {
194	            char name[TINYFS_NAME_MAX];
195	            if (next_word(&p, name, sizeof(name)) == 0) {
196	                print_line("usage: run <file>");
197	            } else {
198	                cmd_run(name);
199	            }
200	        } else if (strcmp(cmd, "write") == 0) {
201	            char name[TINYFS_NAME_MAX];
202	            if (next_word(&p, name, sizeof(name)) == 0) {
203	                print_line("usage: write <file> <text>");
204	            } else {
205	                skip_spaces(&p);
206	                cmd_write(name, p);
207	            }
208	        } else {
209	            print_line("unknown command (type 'help')");
210	        }
211	    }
212	}
213	
Edit · Jul 17, 19:36:44 · toolu_01UrvDuRf21o2gMe8QZoABWY
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
- #include "console.h"
- #include "fs/tinyfs.h"
- #include "keyboard.h"
- #include "lang/script/eval.h"
- #include "serial.h"
- #include "string.h"
+ #include "console.h"
+ #include "fs/tinyfs.h"
+ #include "keyboard.h"
+ #include "lang/compiler/codegen.h"
+ #include "lang/compiler/parser.h"
+ #include "lang/script/eval.h"
+ #include "lang/vm.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:36:52 · toolu_01L9tXeTvQkBpK6CS1djEoue
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
55 lines
- static void cmd_rm(const char *name) {
+ static void cmd_build(const char *src_name, const char *out_name) {
+     static char src[TINYFS_MAX_FILE_SIZE + 1];
+     uint32_t src_len = 0;
+     if (tinyfs_read(src_name, src, sizeof(src) - 1, &src_len) != 0) {
+         print_line("build: no such file");
+         return;
+     }
+     src[src_len] = '\0';
+ 
+     tc_parser_t p;
+     tc_parser_init(&p, src);
+     ast_node_t *prog = tc_parse_program(&p);
+     if (p.error) {
+         console_write("build: parse error: ");
+         console_write(p.error_msg);
+         console_write("\n");
+         serial_write("build: parse error: ");
+         serial_write(p.error_msg);
+         serial_write("\n");
+         return;
+     }
+ 
+     static uint8_t out_buf[TINYFS_MAX_FILE_SIZE];
+     uint32_t out_len = 0;
+     char err[64];
+     if (codegen_compile(prog, out_buf, sizeof(out_buf), &out_len, err, sizeof(err)) != 0) {
+         console_write("build: compile error: ");
+         console_write(err);
+         console_write("\n");
+         serial_write("build: compile error: ");
+         serial_write(err);
+         serial_write("\n");
+         return;
+     }
+ 
+     tinyfs_create(out_name);
+     if (tinyfs_write(out_name, out_buf, out_len) != 0) {
+         print_line("build: failed to write output file");
+         return;
+     }
+     print_line("build: ok");
+ }
+ 
+ static void cmd_exec(const char *name) {
+     static uint8_t buf[TINYFS_MAX_FILE_SIZE];
+     uint32_t len = 0;
+     if (tinyfs_read(name, buf, sizeof(buf), &len) != 0) {
+         print_line("exec: no such file");
+         return;
+     }
+     vm_run(buf, len);
+ }
+ 
+ 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:36:57 · toolu_01HmXwSskASjRkcBVX9M7HEz
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
- static void cmd_help(void) {
-     print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>, help, clear, reboot");
- }
+ static void cmd_help(void) {
+     print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,");
+     print_line("          build <src> <out>, exec <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)
Edit · Jul 17, 19:37:03 · toolu_01Bw7DBGo4kxQuCYJNK3osnk
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
34 lines
-         } else if (strcmp(cmd, "write") == 0) {
-             char name[TINYFS_NAME_MAX];
-             if (next_word(&p, name, sizeof(name)) == 0) {
-                 print_line("usage: write <file> <text>");
-             } else {
-                 skip_spaces(&p);
-                 cmd_write(name, p);
-             }
-         } else {
+         } else if (strcmp(cmd, "write") == 0) {
+             char name[TINYFS_NAME_MAX];
+             if (next_word(&p, name, sizeof(name)) == 0) {
+                 print_line("usage: write <file> <text>");
+             } else {
+                 skip_spaces(&p);
+                 cmd_write(name, p);
+             }
+         } else if (strcmp(cmd, "build") == 0) {
+             char src_name[TINYFS_NAME_MAX];
+             char out_name[TINYFS_NAME_MAX];
+             if (next_word(&p, src_name, sizeof(src_name)) == 0 ||
+                 next_word(&p, out_name, sizeof(out_name)) == 0) {
+                 print_line("usage: build <src> <out>");
+             } else {
+                 cmd_build(src_name, out_name);
+             }
+         } else if (strcmp(cmd, "exec") == 0) {
+             char name[TINYFS_NAME_MAX];
+             if (next_word(&p, name, sizeof(name)) == 0) {
+                 print_line("usage: exec <file>");
+             } else {
+                 cmd_exec(name);
+             }
+         } else {
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:37:07
Now update the Makefile with the new sources and build directories:
Edit · Jul 17, 19:37:11 · toolu_01NJ7MQPYzLbeF2AmTgDtjt9
/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/lang/lexer.c kernel/lang/script/ast.c \
-                  kernel/lang/script/parser.c kernel/lang/script/eval.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 \
+                  kernel/lang/compiler/parser.c kernel/lang/compiler/codegen.c \
+                  kernel/lang/vm.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:37:15 · toolu_01MxLKmMG1LEgFGob5D2QCg9
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- BUILD_SUBDIRS := $(BUILD) $(BUILD)/fs $(BUILD)/shell $(BUILD)/lang $(BUILD)/lang/script
+ BUILD_SUBDIRS := $(BUILD) $(BUILD)/fs $(BUILD)/shell $(BUILD)/lang $(BUILD)/lang/script $(BUILD)/lang/compiler
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)
Bash · Jul 17, 19:37:18 · toolu_01EAKubjsA9zexdH3zo21s5x
Build with Phase 6 compiler/VM additions
$ make clean >/dev/null && make 2>&1
result
42 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
mkdir -p build/lang/compiler
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
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/compiler/parser.o kernel/lang/compiler/parser.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/compiler/codegen.o kernel/lang/compiler/codegen.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/lang/vm.o kernel/lang/vm.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 build/lang/compiler/parser.o build/lang/compiler/codegen.o build/lang/vm.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)
Bash · Jul 17, 19:37:24 · toolu_01SFrvbwmx4AZe6BsweHmSnm
Check kernel size against budget
$ stat -c%s build/kernel.bin
result
47416
agent · Jul 17, 19:37:27
Well within the 256KB (512-sector) budget now. Let's create the `.tc` test fixture:
Write · Jul 17, 19:37:29 · toolu_01CziW64dFWPbrBh9TGTZWcu
/home/martin/workspace/prototypes/boot-to-claude/tests/hello.tc
x = 2 + 3 * 4;
print(x);

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

func fib(n) {
    if (n < 2) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}
print(fib(10));

i = 0;
sum = 0;
while (i < 5) {
    sum = sum + i;
    i = i + 1;
}
print(sum);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/hello.tc (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:37:32 · toolu_01Jx5ao2quzoGrXCcEDDyDUF
/home/martin/workspace/prototypes/boot-to-claude/tests/expected/hello.tc.txt
14
7
55
10
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/expected/hello.tc.txt (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:37:34
Now wire the seed into the Makefile and extend `run_tests.sh`:
Edit · Jul 17, 19:37:36 · toolu_011Te7zxbmfK2pgyoda5ZuPA
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- MKFS_SEED_FILES := tests/hello.tsh
- MKFS_SEED_SPECS := tests/hello.tsh:hello.tsh
+ MKFS_SEED_FILES := tests/hello.tsh tests/hello.tc
+ MKFS_SEED_SPECS := tests/hello.tsh:hello.tsh tests/hello.tc:hello.tc
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:37:41 · toolu_01RdkyU8uDzC6vFBPaCVbvNu
/home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh
- type_line "run hello.tsh"
- check_fixture_output tests/expected/hello.tsh.txt
- 
- echo "run_tests: PASS"
+ type_line "run hello.tsh"
+ check_fixture_output tests/expected/hello.tsh.txt
+ 
+ type_line "build hello.tc hello.tbc"
+ wait_for "build: ok"
+ 
+ type_line "exec hello.tbc"
+ check_fixture_output tests/expected/hello.tc.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:37:44 · toolu_01NQ3YrcPh7DNSWutYyCkeQE
Rebuild and run full test suite including the .tc compiler fixture
$ make 2>&1 | tail -3 && ./tools/run_tests.sh 2>&1 | tail -40
result
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 tests/hello.tc:hello.tc
mkfs_tinyfs: formatted build/disk.img (3446 data blocks available)
run_tests: PASS
qemu-system-i386: terminating on signal 15 from pid 1900847 (bash)
agent · Jul 17, 19:37:58
Excellent — recursive `fib(10)` correctly returning 55 is strong evidence the call-frame/locals-stack machinery works. Committing Phase 6.
Bash · Jul 17, 19:37:59 · toolu_01JG9kxRARHjak1ETHrmkBTM
Check for stray processes and stage Phase 6 files
$ pgrep -af qemu-system-i386 || echo "no stray qemu"; git add -A && git status --short
result
1900990 /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"; git add -A && git status --short' < /dev/null && pwd -P >| /tmp/claude-e2ac-cwd
M  Makefile
A  docs/bytecode-isa.md
A  kernel/lang/bytecode.h
A  kernel/lang/compiler/codegen.c
A  kernel/lang/compiler/codegen.h
A  kernel/lang/compiler/parser.c
A  kernel/lang/compiler/parser.h
A  kernel/lang/vm.c
A  kernel/lang/vm.h
M  kernel/shell/shell.c
A  tests/expected/hello.tc.txt
A  tests/hello.tc
M  tools/run_tests.sh